flydata 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0c46afc6ca25cb5f8969e22aac032415a315eb0e
4
- data.tar.gz: a95cdb85e846ae1b56f4ad3d2802fc163ec9df99
3
+ metadata.gz: 83cb9df24d4fe0b983b20bfd30d1798aa4cbecdd
4
+ data.tar.gz: a00a24a84acf688553503bf97b156acdd57fc8ab
5
5
  SHA512:
6
- metadata.gz: 56cb5f8d6e58f71598bfc370ea31a24784c1132b5fcadbae263cb29c85a1cd950ff0c2169698722f73058c9d44b16424a57d441c4b98f1ee914ddd57062d91df
7
- data.tar.gz: c0b6985cafa887eb2a8aac272c57431f1bb72343b937be7ebb55ef5dfdfec989778acf160119bd0205e4978289fd4134c04890a468bc655022dc99d17f65107f
6
+ metadata.gz: 335e28547f000af29cf5df8d7a28632fe8fa5356298016f80819622c2a8299ae8469aa46a4e2ec8aa6179c472d151ea90583da1a7ab5f9367aec7668200c9ebc
7
+ data.tar.gz: 78049be459795e1eccb4b486c55e74989b8f2fae7cce9e5a8ce79236a12ad54d990550b30e71f9027e3ff299598d9f586a63fff8911ae159c6c0935f95e2bfeb
data/Gemfile CHANGED
@@ -13,7 +13,7 @@ gem "slop", '~> 3.4', '>= 3.4.6'
13
13
  gem "treetop", '~> 1.5', '>= 1.5.3'
14
14
  gem "sys-filesystem", '~> 1.1', '>= 1.1.3'
15
15
  gem "io-console", '~> 0.4.2', '>= 0.4.2'
16
- gem "kodama", '~> 0.1.2', '>= 0.1.5'
16
+ gem "kodama", '~> 0.1.2', '>= 0.1.6'
17
17
  gem "serverengine", '~> 1.5'
18
18
 
19
19
  group :development do
@@ -61,7 +61,7 @@ GEM
61
61
  rdoc
62
62
  json (1.8.1)
63
63
  jwt (1.0.0)
64
- kodama (0.1.5)
64
+ kodama (0.1.6)
65
65
  ruby-binlog (~> 1.0, >= 1.0.4)
66
66
  method_source (0.8.2)
67
67
  mime-types (2.3)
@@ -135,7 +135,7 @@ DEPENDENCIES
135
135
  io-console (~> 0.4.2, >= 0.4.2)
136
136
  jeweler (~> 1.8, >= 1.8.8)
137
137
  json (~> 1.8, >= 1.8.0)
138
- kodama (~> 0.1.2, >= 0.1.5)
138
+ kodama (~> 0.1.2, >= 0.1.6)
139
139
  mysql2 (~> 0.3, >= 0.3.17)
140
140
  protected_attributes (~> 1.0, >= 1.0.8)
141
141
  pry
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.0
1
+ 0.4.1
@@ -304,6 +304,18 @@ class TableDefError < StandardError
304
304
  end
305
305
  end
306
306
 
307
+ ## Compatibility check error
308
+
309
+ class CompatibilityError < StandardError
310
+ end
311
+
312
+ class AgentCompatibilityError < StandardError
313
+ end
314
+
315
+ class MysqlCompatibilityError < StandardError
316
+ end
317
+
318
+
307
319
  ## Error container
308
320
 
309
321
  class DataDeliveryErrorThreadContext
@@ -1,9 +1,9 @@
1
1
  require 'open3'
2
2
  require 'flydata-core/table_def/mysql_table_def'
3
3
 
4
- module Flydata
4
+ module FlydataCore
5
5
  module Mysql
6
- class MysqlUtil
6
+ class CommandGenerator
7
7
  DEFAULT_MYSQL_CMD_OPTION = "--default-character-set=utf8 --protocol=tcp"
8
8
 
9
9
  # Generate mysql/mysqldump command with options
@@ -76,7 +76,34 @@ module Flydata
76
76
  generate_mysql_cmd(opt)
77
77
  end
78
78
 
79
- def self.each_mysql_tabledef(tables, option)
79
+ def self.each_mysql_tabledef(tables, options, &block)
80
+ tables = tables.clone
81
+ missing_tables = []
82
+ begin
83
+ if tables.to_s == '' || tables.to_s == '[]'
84
+ raise ArgumentError, "tables is nil or empty"
85
+ end
86
+ _each_mysql_tabledef(tables, options, &block)
87
+ rescue TableMissingError => e
88
+ tables.delete e.table
89
+ missing_tables << e.table
90
+ return missing_tables if tables.empty?
91
+ retry
92
+ end
93
+ missing_tables
94
+ end
95
+
96
+ private
97
+
98
+ class TableMissingError < RuntimeError
99
+ def initialize(message, table)
100
+ super(message)
101
+ @table = table
102
+ end
103
+ attr_reader :table
104
+ end
105
+
106
+ def self._each_mysql_tabledef(tables, option)
80
107
  command = generate_mysql_ddl_dump_cmd(option.merge(tables: tables))
81
108
 
82
109
  create_opt = {}
@@ -100,14 +127,20 @@ module Flydata
100
127
  errors = ""
101
128
  while !stderr.eof?
102
129
  line = stderr.gets.gsub('mysqldump: ', '')
103
- errors << line unless /Warning: Using a password on the command line interface can be insecure./ === line
130
+ case line
131
+ when /Couldn't find table: "([^"]+)"/
132
+ missing_table = $1
133
+ raise TableMissingError.new(line, missing_table)
134
+ when /Warning: Using a password on the command line interface can be insecure./
135
+ # Ignore
136
+ else
137
+ errors << line
138
+ end
104
139
  end
105
140
  raise errors unless errors.empty?
106
141
  end
107
142
  end
108
143
 
109
- private
110
-
111
144
  def self.convert_keys_to_sym(hash)
112
145
  hash.inject(hash.dup) do |ret, (k, v)|
113
146
  if k.kind_of?(String) && ret[k.to_sym].nil?
@@ -0,0 +1,222 @@
1
+ require 'flydata-core/errors'
2
+ require 'mysql2'
3
+
4
+ module FlydataCore
5
+ module Mysql
6
+ class CompatibilityChecker
7
+ def initialize(option = {})
8
+ @option = option || {}
9
+ end
10
+
11
+ def do_check(option = @option, &block)
12
+ result = block.call create_query(option)
13
+ check_result(result, option)
14
+ end
15
+
16
+ # Override
17
+ #def create_query(option = @option)
18
+ #end
19
+
20
+ # Override
21
+ #def validate_result(result, option = @option)
22
+ #end
23
+ end
24
+
25
+ class MysqlCompatibilityChecker < CompatibilityChecker
26
+ def do_check(option = @option, &block)
27
+ query = create_query(option)
28
+ result = if block
29
+ block.call query
30
+ else
31
+ exec_query(query)
32
+ end
33
+ check_result(result, option)
34
+ end
35
+
36
+ def exec_query(query)
37
+ begin
38
+ client = Mysql2::Client.new(@option)
39
+ client.query(query)
40
+ ensure
41
+ client.close rescue nil if client
42
+ end
43
+ end
44
+ end
45
+
46
+ class SyncPermissionChecker < MysqlCompatibilityChecker
47
+ def create_query(option = @option)
48
+ "SHOW GRANTS"
49
+ end
50
+
51
+ def check_result(result, option = @option)
52
+ databases = ['mysql', @option[:database]]
53
+ get_grant_regex = /GRANT (?<privs>.*) ON (`)?(?<db_name>[^`]*)(`)?\.\* TO '#{@option[:username]}/
54
+ necessary_permission_fields = ["SELECT","RELOAD","LOCK TABLES","REPLICATION SLAVE","REPLICATION CLIENT"]
55
+ all_privileges_field = ["ALL PRIVILEGES"]
56
+
57
+ found_priv = Hash[databases.map {|d| [d,[]]}]
58
+ missing_priv = {}
59
+
60
+ result.each do |res|
61
+ # SHOW GRANTS should only return one column
62
+ res_value = res.values.first
63
+ matched_values = res_value.match(get_grant_regex)
64
+ next unless matched_values
65
+ line_priv = matched_values["privs"].split(", ")
66
+ if matched_values["db_name"] == "*"
67
+ return true if (all_privileges_field - line_priv).empty?
68
+ databases.each {|d| found_priv[d] << line_priv }
69
+ elsif databases.include? matched_values["db_name"]
70
+ if (all_privileges_field - line_priv).empty?
71
+ found_priv[matched_values["db_name"]] = necessary_permission_fields
72
+ else
73
+ found_priv[matched_values["db_name"]] << line_priv
74
+ end
75
+ end
76
+ missing_priv = get_missing_privileges(found_priv, necessary_permission_fields)
77
+ return true if missing_priv.empty?
78
+ end
79
+ error_text = "The user '#{@option[:username]}' does not have the correct permissions to run FlyData Sync\n"
80
+ error_text << " * These privileges are missing...\n"
81
+ missing_priv.each_key {|db| error_text << " for the database '#{db}': #{missing_priv[db].join(", ")}\n"}
82
+ raise MysqlCompatibilityError, error_text
83
+ end
84
+
85
+ private
86
+
87
+ def get_missing_privileges(found_priv, all_priv)
88
+ return_hash = {}
89
+ found_priv.each_key do |key|
90
+ missing_priv = all_priv - found_priv[key].flatten.uniq
91
+ return_hash[key] = missing_priv unless missing_priv.empty?
92
+ end
93
+ return_hash
94
+ end
95
+ end
96
+
97
+ module MysqlVariablesHandling
98
+ def create_query(option = @option)
99
+ "SHOW VARIABLES;"
100
+ end
101
+
102
+ def convert_result_to_hash(result)
103
+ ret = {}
104
+ result.each do |record|
105
+ k = record['Variable_name']
106
+ v = record['Value']
107
+ ret[k] = v
108
+ end
109
+ ret
110
+ end
111
+ end
112
+
113
+ class BinlogParameterChecker < MysqlCompatibilityChecker
114
+ include MysqlVariablesHandling
115
+
116
+ SYS_VAR_TO_CHECK = {
117
+ # parameter => expected value
118
+ 'binlog_format'=>'ROW',
119
+ 'binlog_checksum'=>'NONE',
120
+ 'log_bin_use_v1_row_events'=>'ON',
121
+ 'log_slave_updates'=>'ON'
122
+ }
123
+
124
+ def check_result(result, option = @option)
125
+ errors = {}
126
+ param_hash = convert_result_to_hash(result)
127
+
128
+ SYS_VAR_TO_CHECK.each_key do |sys_var|
129
+ if param_hash.has_key?(sys_var)
130
+ actual_val = param_hash[sys_var]
131
+ expected_val = SYS_VAR_TO_CHECK[sys_var]
132
+ unless actual_val == expected_val
133
+ errors[sys_var] = actual_val
134
+ end
135
+ elsif not %w(binlog_checksum log_bin_use_v1_row_events).include?(sys_var)
136
+ # Mark variables that do not exist as error
137
+ errors[sys_var] = false
138
+ end
139
+ end
140
+
141
+ unless errors.empty?
142
+ error_explanation = ""
143
+ errors.each_key do |err_key|
144
+ error_explanation << "\n * #{err_key} is #{errors[err_key]} but should be #{SYS_VAR_TO_CHECK[err_key]}"
145
+ end
146
+ raise FlydataCore::MysqlCompatibilityError,
147
+ "These system variable(s) are not the correct value: #{error_explanation}\n" +
148
+ " Please change these system variables for FlyData Sync to run correctly"
149
+ end
150
+ end
151
+ end
152
+
153
+ class TableTypeChecker < MysqlCompatibilityChecker
154
+ SELECT_TABLE_INFO_TMPLT =
155
+ "SELECT table_name, table_type, engine " +
156
+ "FROM information_schema.tables " +
157
+ "WHERE table_schema = '%s' and table_name in (%s)"
158
+
159
+ # option[:client] : Mysql2::Client object
160
+ # option[:tables] : target table list
161
+ def create_query(option = @option)
162
+ SELECT_TABLE_INFO_TMPLT % [
163
+ Mysql2::Client.escape(option[:database]),
164
+ option[:tables].collect{|t| "'#{Mysql2::Client.escape(t)}'"}.join(", ")
165
+ ]
166
+ end
167
+
168
+ def check_result(result, option = @option)
169
+ invalid_tables = []
170
+ result.each do |r|
171
+ invalid_tables.push(r['table_name']) if r['table_type'] == 'VIEW' || r['engine'] == 'MEMORY'
172
+ end
173
+
174
+ unless invalid_tables.empty?
175
+ raise FlydataCore::MysqlCompatibilityError,
176
+ "FlyData does not support VIEW and MEMORY ENGINE table. " +
177
+ "Remove following tables from data entry: #{invalid_tables.join(", ")}"
178
+ end
179
+ end
180
+ end
181
+
182
+ class NonRdsRetentionChecker < MysqlCompatibilityChecker
183
+ include MysqlVariablesHandling
184
+
185
+ BINLOG_RETENTION_HOURS = 24
186
+ EXPIRE_LOGS_DAYS_LIMIT = BINLOG_RETENTION_HOURS / 24
187
+
188
+ def check_result(result, option = @option)
189
+ param_hash = convert_result_to_hash(result)
190
+
191
+ if param_hash["expire_logs_days"].to_s != '0' &&
192
+ param_hash["expire_logs_days"].to_i <= EXPIRE_LOGS_DAYS_LIMIT
193
+ raise FlydataCore::MysqlCompatibilityError,
194
+ "Binary log retention is too short\n " +
195
+ " We recommend the system variable '@@expire_logs_days' to be either set to 0 or at least #{EXPIRE_LOGS_DAYS_LIMIT} day(s)"
196
+ end
197
+ end
198
+ end
199
+
200
+ class RdsRetentionChecker < MysqlCompatibilityChecker
201
+ BINLOG_RETENTION_HOURS = NonRdsRetentionChecker::BINLOG_RETENTION_HOURS
202
+
203
+ def create_query(option = @option)
204
+ "call mysql.rds_show_configuration;"
205
+ end
206
+
207
+ def check_result(result, option = @option)
208
+ if result.first["name"] == "binlog retention hours"
209
+ if result.first["value"].nil? ||
210
+ result.first["value"].to_i <= BINLOG_RETENTION_HOURS
211
+ raise FlydataCore::MysqlCompatibilityError,
212
+ "Binary log retention is too short\n" +
213
+ " We recommend setting RDS binlog retention " +
214
+ "to be at least #{BINLOG_RETENTION_HOURS} hours. To do this, " +
215
+ "run this on your RDS MySQL database:\n" +
216
+ " $> call mysql.rds_set_configuration('binlog retention hours', 94);"
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,7 @@
1
+ # QueryJob is a library provding an interfact to the Query Job Processor (also
2
+ # known as Copy Handler)
3
+ # SimpleDB related classes and methods defined in flydata and flydata-web will
4
+ # be eventually consolidated into this library.
5
+ Dir[File.join(File.dirname(__FILE__), "query_job/**/*.rb")].each do |f|
6
+ require f
7
+ end
@@ -0,0 +1,27 @@
1
+ require 'flydata-core/table_def/redshift_table_def'
2
+
3
+ module FlydataCore
4
+ module QueryJob
5
+
6
+ class Redshift
7
+ # returns a list of table names used in the Query Job system.
8
+ def self.target_table_names(flydata_table_names)
9
+ flydata_table_names = [ flydata_table_names ] unless flydata_table_names.kind_of?(Array)
10
+
11
+ flydata_table_names.collect {|flydata_table_name|
12
+ redshift_table_name = TableDef::RedshiftTableDef.convert_to_valid_table_name(flydata_table_name)
13
+ table_names = [ redshift_table_name ]
14
+ if redshift_table_name != flydata_table_name
15
+ # for backward compatibility
16
+ # Old implementation used FlyData table names. To support it, the
17
+ # method adds the flydata_table_name as well.
18
+ table_names << flydata_table_name
19
+ end
20
+
21
+ table_names
22
+ }.flatten
23
+ end
24
+ end
25
+
26
+ end
27
+ end
@@ -1,10 +1,10 @@
1
1
  require 'spec_helper'
2
- require 'flydata/mysql/mysql_util'
2
+ require 'flydata-core/mysql/command_generator'
3
3
 
4
- module Flydata
4
+ module FlydataCore
5
5
  module Mysql
6
6
 
7
- describe MysqlUtil do
7
+ describe CommandGenerator do
8
8
  let(:default_option) { {
9
9
  command: 'mysqldump',
10
10
  host: 'test-host',
@@ -139,6 +139,25 @@ module Flydata
139
139
  'mysql -h test-host -P 3306 -utestuser -p"testpassword" --default-character-set=utf8 --protocol=tcp -e "SHOW GRANTS;" testdb'
140
140
  ) }
141
141
  end
142
+
143
+ describe '.each_mysql_tabledef' do
144
+ context 'with empty tables' do
145
+ subject { described_class.each_mysql_tabledef([], nil) }
146
+ it do
147
+ expect{subject}.to raise_error(
148
+ ArgumentError,"tables is nil or empty")
149
+ end
150
+ end
151
+
152
+ context 'when all tables are missing' do
153
+ before do
154
+ allow(described_class).to receive(:_each_mysql_tabledef).
155
+ and_raise(CommandGenerator::TableMissingError.new('table, missing error', 'table1'))
156
+ end
157
+ subject { described_class.each_mysql_tabledef(%w(table1), nil) }
158
+ it { is_expected.to eq(%w(table1)) }
159
+ end
160
+ end
142
161
  end
143
162
  end
144
163
  end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ module FlydataCore
4
+ module Mysql
5
+ describe SyncPermissionChecker do
6
+
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+ require 'flydata-core/query_job/redshift'
3
+
4
+ module FlydataCore::QueryJob
5
+
6
+ describe Redshift do
7
+ describe '.target_table_names' do
8
+ subject { described_class.target_table_names(flydata_table_names) }
9
+
10
+ context 'with a single table name' do
11
+ let(:flydata_table_names) { "my_table" }
12
+
13
+ it { is_expected.to eq [ flydata_table_names ] }
14
+ end
15
+
16
+ context 'with multiples table names' do
17
+ let(:flydata_table_names) { %w(my_table1 my_table2) }
18
+
19
+ it { is_expected.to eq flydata_table_names }
20
+ end
21
+ context 'with a mixed case table name' do
22
+ let(:flydata_table_names) { "MyTable" }
23
+
24
+ it { is_expected.to eq [ flydata_table_names.downcase, flydata_table_names ] }
25
+ end
26
+ context 'with table names including mixed case one' do
27
+ let(:flydata_table_names) { %w(MyTable my_table2) }
28
+
29
+ it { is_expected.to eq %w(mytable MyTable my_table2) }
30
+ end
31
+ end
32
+ end
33
+
34
+ end