flydata 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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