repctl 0.0.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.
@@ -0,0 +1,638 @@
1
+ require 'mysql2'
2
+ require 'fileutils'
3
+ require 'delegate'
4
+ require 'open3'
5
+
6
+ module Repctl
7
+
8
+ class Client < DelegateClass(Mysql2::Client)
9
+ include Servers
10
+
11
+ @@clients = {}
12
+
13
+ def initialize(instance, opts)
14
+ @instance = instance
15
+ server = server_for_instance(@instance)
16
+ options = {
17
+ :host => server['hostname'],
18
+ :username => "root",
19
+ :port => server['port'],
20
+ :password => Config::ROOT_PASSWORD
21
+ }
22
+ options.delete(:password) if opts[:no_password]
23
+ @client = Mysql2::Client.new(options)
24
+ super(@client)
25
+ end
26
+
27
+ def self.open(instance, opts = {})
28
+ timeout = opts[:timeout] || 10
29
+ opts.delete(:timeout)
30
+ begin
31
+ instance = Integer(instance)
32
+ rescue Mysql2::Error => e
33
+ puts "Instance value <#{instance}> is invalid."
34
+ else
35
+ timeout = Integer(timeout)
36
+ while timeout >= 0
37
+ begin
38
+ @@clients[instance] ||= Client.new(instance, opts)
39
+ # puts "Connected to instance #{instance}."
40
+ break
41
+ rescue Mysql2::Error => e
42
+ puts "#{e.message}, retrying connection to instance #{instance}..."
43
+ # puts e.backtrace
44
+ sleep 1
45
+ timeout -= 1
46
+ end
47
+ end
48
+ end
49
+ @@clients[instance]
50
+ end
51
+
52
+ def close
53
+ @@clients[@instance] = nil
54
+ @client.close
55
+ end
56
+
57
+ def reset
58
+ @client.close
59
+ @@clients[@instance] = nil
60
+ Client.open(@instance)
61
+ end
62
+
63
+ end
64
+
65
+ module Commands
66
+
67
+ include FileUtils
68
+ include Repctl::Config
69
+ include Servers
70
+
71
+ def do_secure_accounts(instance)
72
+ client = Client.open(instance, :no_password => true)
73
+ q1 = "UPDATE mysql.user SET Password = PASSWORD(\'#{ROOT_PASSWORD}\') where User = \'root\'"
74
+ q2 = "UPDATE mysql.user SET Password = PASSWORD(\'#{ROOT_PASSWORD}\') where User = \'\'"
75
+ # q3 = "CREATE USER \'root\'@\'%.#{REPLICATION_DOMAIN}\' IDENTIFIED BY \'#{ROOT_PASSWORD}\'"
76
+ # For testing with clients whose DHCP assigned IP address is not in DNS.
77
+ q3 = "CREATE USER \'root\'@\'%' IDENTIFIED BY \'#{ROOT_PASSWORD}\'"
78
+ q4 = "GRANT ALL PRIVILEGES ON *.* to \'root\'@\'%\' WITH GRANT OPTION"
79
+ q5 = "FLUSH PRIVILEGES"
80
+ if client
81
+ [q1, q2, q3, q4, q5].each do |query|
82
+ puts query
83
+ client.query(query)
84
+ end
85
+ end
86
+ rescue Mysql2::Error => e
87
+ puts e.message
88
+ ensure
89
+ client.close if client
90
+ end
91
+
92
+ def do_start(instance)
93
+ pid = get_mysqld_pid(instance)
94
+ if pid
95
+ puts "Instance #{instance} with PID #{pid} is already running."
96
+ else
97
+ pid = fork
98
+ unless pid
99
+ # We're in the child.
100
+ puts "Starting instance #{instance} with PID #{Process.pid}."
101
+ server = server_for_instance(instance)
102
+
103
+ exec(["#{MYSQL_HOME}/bin/mysqld", "mysqld"],
104
+ "--defaults-file=#{server['defaults-file']}",
105
+ "--datadir=#{server['datadir']}",
106
+ "--port=#{server['port']}",
107
+ "--server-id=#{server['server-id']}",
108
+ "--innodb_data_home_dir=#{server['innodb_data_home_dir']}",
109
+ "--innodb_log_group_home_dir=#{server['innodb_log_group_home_dir']}",
110
+ "--relay-log=#{Socket.gethostname}-relay-bin",
111
+ "--socket=#{server['socket']}",
112
+ "--user=mysql")
113
+ end
114
+ end
115
+ end
116
+
117
+ def do_admin(instance, operation)
118
+ server = server_for_instance(instance)
119
+
120
+ cmd = "#{MYSQL_HOME}/bin/mysqladmin " +
121
+ "--defaults-file=#{server['defaults-file']} " +
122
+ "--user=root " +
123
+ "--host=#{server['hostname']} " +
124
+ "--port=#{server['port']} " +
125
+ "--password=#{ROOT_PASSWORD} " +
126
+ operation
127
+
128
+ pid = get_mysqld_pid(instance)
129
+ if pid
130
+ puts "Running #{operation} on instance #{instance} with pid #{pid}."
131
+ run_cmd(cmd, false)
132
+ else
133
+ puts "Instance #{instance} is not running."
134
+ end
135
+ end
136
+
137
+ def do_config(instance)
138
+ server = server_for_instance(instance)
139
+ FileUtils.rm_rf(server['datadir'])
140
+ cmd = "./scripts/mysql_install_db " +
141
+ "--defaults-file=#{server['defaults-file']} " +
142
+ "--datadir=#{server['datadir']} " +
143
+ "--server-id=#{server['server-id']} " +
144
+ "--innodb_data_home_dir=#{server['innodb_data_home_dir']} " +
145
+ "--innodb_log_group_home_dir=#{server['innodb_log_group_home_dir']} " +
146
+ "--relay-log=#{Socket.gethostname}-relay-bin"
147
+ %x( cd #{MYSQL_HOME} && #{cmd} )
148
+ end
149
+
150
+ def do_status(instance)
151
+
152
+ status = get_coordinates(instance)
153
+ puts status
154
+ end
155
+
156
+ #
157
+ # Treat the instance as a slave and
158
+ # process the output of "SHOW SLAVE STATUS".
159
+ #
160
+ def get_slave_status(instance)
161
+ keys = [
162
+ "Instance",
163
+ "Error",
164
+ "Slave_IO_State",
165
+ "Slave_IO_Running",
166
+ "Slave_SQL_Running",
167
+ "Last_IO_Error",
168
+ "Last_SQL_Error",
169
+ "Seconds_Behind_Master",
170
+ "Master_Log_File",
171
+ "Read_Master_Log_Pos",
172
+ "Relay_Master_Log_File",
173
+ "Exec_Master_Log_Pos",
174
+ "Relay_Log_File",
175
+ "Relay_Log_Pos"
176
+ ]
177
+ results = {}
178
+ status = do_slave_status(instance)
179
+ keys.each do |k|
180
+ results.merge!(k => status[k]) if (status[k] and status[k] != "")
181
+ end
182
+ results
183
+ end
184
+
185
+ def do_crash(instance)
186
+ pid = get_mysqld_pid(instance)
187
+ puts "pid is #{pid}"
188
+ if pid
189
+ puts "Killing mysqld instance #{instance} with PID #{pid}"
190
+ Process.kill("KILL", pid.to_i)
191
+ while get_mysqld_pid(instance)
192
+ puts "in looop"
193
+ sleep 1
194
+ end
195
+ puts "MySQL server instance #{instance.to_i} has been killed."
196
+ else
197
+ puts "MySQL server instance #{instance.to_i} is not running."
198
+ end
199
+ end
200
+
201
+ def is_master?(instance)
202
+ get_slave_coordinates(instance).empty?
203
+ end
204
+
205
+ def is_slave?(instance)
206
+ !is_master?(instance)
207
+ end
208
+
209
+ # This is an example template to create commands to issue queries.
210
+ def do_slave_status(instance)
211
+ client = Client.open(instance)
212
+ if client
213
+ results = client.query("SHOW SLAVE STATUS")
214
+ results.each_with_index do |index, line|
215
+ puts "#{index}: #{line}"
216
+ end
217
+ else
218
+ puts "Could not open connection to MySQL instance #{instance}."
219
+ end
220
+ rescue Mysql2::Error => e
221
+ puts e.message
222
+ ensure
223
+ client.close if client
224
+ end
225
+
226
+ def find_masters()
227
+ masters = []
228
+ all_servers.each do |s|
229
+ masters << s if is_master?(s)
230
+ end
231
+ masters
232
+ end
233
+
234
+ #
235
+ # From http://dev.mysql.com/doc/refman/5.0/en/lock-tables.html:
236
+ #
237
+ # For a filesystem snapshot of innodb, we find that setting
238
+ # innodb_max_dirty_pages_pct to zero; doing a 'flush tables with
239
+ # readlock'; and then waiting for the innodb state to reach 'Main thread
240
+ # process no. \d+, id \d+, state: waiting for server activity' is
241
+ # sufficient to quiesce innodb.
242
+ #
243
+ # You will also need to issue a slave stop if you're backing up a slave
244
+ # whose relay logs are being written to its data directory.
245
+ #
246
+ #
247
+ # select @@innodb_max_dirty_pages_pct;
248
+ # flush tables with read lock;
249
+ # show master status;
250
+ # ...freeze filesystem; do backup...
251
+ # set global innodb_max_dirty_pages_pct = 75;
252
+ #
253
+
254
+ def do_change_master(master, slave, coordinates)
255
+ master_server = server_for_instance(master)
256
+ begin
257
+ slave_connection = Client.open(slave)
258
+ if slave_connection
259
+
260
+ # Replication on the slave can't be running if we want to execute
261
+ # CHANGE MASTER TO.
262
+ slave_connection.query("STOP SLAVE") rescue Mysql2::Error
263
+
264
+ raise "master_server is nil" unless master_server
265
+
266
+ cmd = <<-EOT
267
+ CHANGE MASTER TO
268
+ MASTER_HOST = \'#{master_server['hostname']}\',
269
+ MASTER_PORT = #{master_server['port']},
270
+ MASTER_USER = \'#{REPLICATION_USER}\',
271
+ MASTER_PASSWORD = \'#{REPLICATION_PASSWORD}\',
272
+ MASTER_LOG_FILE = \'#{coordinates[:file]}\',
273
+ MASTER_LOG_POS = #{coordinates[:position]}
274
+ EOT
275
+ puts "Executing: #{cmd}"
276
+ slave_connection.query(cmd)
277
+ else
278
+ puts "do_change_master: Could not connnect to MySQL server."
279
+ end
280
+ rescue Mysql2::Error => e
281
+ puts e.message
282
+ ensure
283
+ slave_connection.close if slave_connection
284
+ end
285
+
286
+ end
287
+
288
+ def do_dump(instance, dumpfile)
289
+ server = server_for_instance(instance)
290
+ coordinates = get_coordinates(instance) do
291
+ cmd = "#{MYSQL_HOME}/bin/mysqldump " +
292
+ "--defaults-file=#{server['defaults-file']} " +
293
+ "--user=root " +
294
+ "--password=#{ROOT_PASSWORD} " +
295
+ "--socket=#{server['socket']} " +
296
+ "--all-databases --lock-all-tables > #{DUMP_DIR}/#{dumpfile}"
297
+ run_cmd(cmd, true)
298
+ end
299
+ coordinates
300
+ end
301
+
302
+ def do_restore(instance, dumpfile)
303
+ server = server_for_instance(instance)
304
+
305
+ # Assumes that the instance is running, but is not acting as a slave.
306
+ cmd = "#{MYSQL_HOME}/bin/mysql " +
307
+ "--defaults-file=#{server['defaults-file']} " +
308
+ "--user=root " +
309
+ "--password=#{ROOT_PASSWORD} " +
310
+ "--socket=#{server['socket']} " +
311
+ "< #{DUMP_DIR}/#{dumpfile}"
312
+ run_cmd(cmd, true)
313
+ end
314
+
315
+ #
316
+ # Get the status of replication for the master and all slaves.
317
+ # Return an array of hashes, each hash has the form:
318
+ # {:instance => <instance id>, :error => <errrmsg>,
319
+ # :master_file => <binlog-file-name>, :master_position => <binlog-position>,
320
+ # :slave_file => <binlog-file-name>, :slave_position => <binlog-position>}
321
+ #
322
+ def do_slave_status(instance)
323
+ instance ||= DEFAULT_MASTER
324
+ locked = false
325
+ client = Client.open(instance, :timeout => 5)
326
+ if client
327
+ client.query("FLUSH TABLES WITH READ LOCK")
328
+ locked = true
329
+ results = client.query("SHOW SLAVE STATUS")
330
+ if results.first
331
+ results.first.merge("Instance" => instance, "Error" => "Success")
332
+ else
333
+ {"Instance" => instance, "Error" => "MySQL server is not a slave."}
334
+ end
335
+ else
336
+ {"Instance" => instance, "Error" => "Could not connect to MySQL server."}
337
+ end
338
+ rescue Mysql2::Error => e
339
+ {:instance => instance, "Error" => e.message}
340
+ ensure
341
+ if client
342
+ client.query("UNLOCK TABLES") if locked
343
+ client.close
344
+ end
345
+ end
346
+
347
+ # Get the master coordinates from a MySQL instance. Optionally,
348
+ # run a block while holding the READ LOCK.
349
+ def get_coordinates(instance)
350
+ instance ||= DEFAULT_MASTER
351
+ locked = false
352
+ client = Client.open(instance)
353
+ if client
354
+ client.query("FLUSH TABLES WITH READ LOCK")
355
+ locked = true
356
+ results = client.query("SHOW MASTER STATUS")
357
+ row = results.first
358
+ coordinates = if row
359
+ {:file => row["File"], :position => row["Position"]}
360
+ else
361
+ {}
362
+ end
363
+ yield coordinates if block_given?
364
+ # You could copy data from the master to the slave at this point
365
+ end
366
+ coordinates
367
+ rescue Mysql2::Error => e
368
+ puts e.message
369
+ # puts e.backtrace
370
+ ensure
371
+ if client
372
+ client.query("UNLOCK TABLES") if locked
373
+ client.close
374
+ end
375
+ # coordinates
376
+ end
377
+
378
+ def get_slave_coordinates(instance)
379
+ client = Client.open(instance)
380
+ if client
381
+ results = client.query("SHOW SLAVE STATUS")
382
+ row = results.first
383
+ if row
384
+ {:file => row["Master_Log_File"], :position => row["Read_Master_Log_Pos"]}
385
+ else
386
+ {}
387
+ end
388
+ end
389
+ ensure
390
+ client.close if client
391
+ end
392
+
393
+ def stop_slave_io_thread(instance)
394
+ client = Client.open(instance)
395
+ if client
396
+ client.query("STOP SLAVE IO_THREAD")
397
+ end
398
+ ensure
399
+ client.close if client
400
+ end
401
+
402
+ def run_mysql_query(instance, cmd)
403
+ client = Client.open(instance)
404
+ if client
405
+ results = client.query(cmd)
406
+ else
407
+ puts "Could not open connection to MySQL instance."
408
+ end
409
+ results
410
+ rescue Mysql2::Error => e
411
+ puts e.message
412
+ puts e.backtrace
413
+ ensure
414
+ client.close if client
415
+ end
416
+
417
+ def start_slave_io_thread(instance)
418
+ client = Client.open(instance)
419
+ if client
420
+ client.query("START SLAVE IO_THREAD")
421
+ end
422
+ ensure
423
+ client.close if client
424
+ end
425
+
426
+ def promote_slave_to_master(instance)
427
+ client = Client.open(instance)
428
+ if client
429
+ client.query("STOP SLAVE")
430
+ client.query("RESET MASTER")
431
+ end
432
+ ensure
433
+ client.close if client
434
+ end
435
+
436
+ def drain_relay_log(instance)
437
+ done = false
438
+ stop_slave_io_thread(instance)
439
+ client = Client.open(instance)
440
+ if client
441
+
442
+ # If the slave 'sql_thread' is not running, this will loop forever.
443
+ while !done
444
+ results = client.query("SHOW PROCESSLIST")
445
+ results.each do |row|
446
+ if row["State"] =~ /Slave has read all relay log/
447
+ done = true
448
+ puts "Slave has read all relay log."
449
+ break
450
+ end
451
+ end
452
+ puts "Waiting for slave to read relay log." unless done
453
+ end
454
+ else
455
+ puts "Could not open connection to instance #{instance}."
456
+ end
457
+ ensure
458
+ client.close if client
459
+ end
460
+
461
+ # 'master' is the new master
462
+ # 'slaves' is contains the list of slaves, one of these may be the current master.
463
+ def switch_master_to(master, slaves)
464
+
465
+ slaves = Array(slaves)
466
+
467
+ # Step 1. Make sure all slaves have completely processed their
468
+ # Relay Log.
469
+ slaves.each do |s|
470
+ puts "Draining relay log for slave instance #{s}"
471
+ drain_relay_log(s) if is_slave?(s)
472
+ end
473
+
474
+ # Step 2. For the slave being promoted to master, issue STOP SLAVE
475
+ # and RESET MASTER.
476
+ client = Client.open(master)
477
+ client.query("STOP SLAVE")
478
+ client.query("RESET MASTER")
479
+ client.close
480
+
481
+ # Step 3. Change the master for the other slaves (and the former master?)
482
+ # XXX this is not complete -- what about the coordinates?
483
+ master_server = server_for_instance(master)
484
+ master_host = 'localhost' # should be master_server[:hostname]
485
+ master_user = 'repl'
486
+ master_password = 'har526'
487
+ master_port = master_server[:port]
488
+ slaves.each do |s|
489
+ client = Client.open(s)
490
+ cmd = <<-EOT
491
+ CHANGE MASTER TO
492
+ MASTER_HOST=\'#{master_host}\',
493
+ MASTER_PORT=#{master_port},
494
+ MASTER_USER=\'#{master_user}\',
495
+ MASTER_PASSWORD=\'#{master_password}\'
496
+ EOT
497
+ client.query(cmd)
498
+ client.query("START SLAVE")
499
+ client.close
500
+ end
501
+ end
502
+
503
+ def do_repl_user(instance)
504
+ hostname = "127.0.0.1"
505
+ client = Client.open(instance)
506
+ cmd = "DROP USER \'#{REPLICATION_USER}\'@\'#{hostname}\'"
507
+ client.query(cmd) rescue Mysql2::Error
508
+
509
+ if client
510
+ # "CREATE USER \'#{REPLICATION_USER\'@'%.thirdmode.com' IDENTIFIED BY \'#{REPLICATION_PASSWORD}\'"
511
+ # "GRANT REPLICATION SLAVE ON *.* TO \'#{REPLICATON_USER}\'@\'%.#{REPLICATION_DOMAIN}\'"
512
+ cmd = "CREATE USER \'#{REPLICATION_USER}\'@\'#{hostname}\' IDENTIFIED BY \'#{REPLICATION_PASSWORD}\'"
513
+ puts cmd
514
+ client.query(cmd)
515
+ cmd = "GRANT REPLICATION SLAVE ON *.* TO \'#{REPLICATION_USER}\'@\'#{hostname}\'"
516
+ puts cmd
517
+ client.query(cmd)
518
+ client.query("FLUSH PRIVILEGES")
519
+ else
520
+ puts "Could not open connection to MySQL instance #{instance}."
521
+ end
522
+ rescue Mysql2::Error => e
523
+ puts e.message
524
+ puts e.backtrace
525
+ ensure
526
+ client.close if client
527
+ end
528
+
529
+
530
+ def do_cluster_user(instance)
531
+ client = Client.open(instance)
532
+
533
+ cmd = "DROP USER \'cluster\'@\'localhost\'"
534
+ client.query(cmd) rescue Mysql2::Error
535
+
536
+ cmd = "DROP USER \'cluster\'@\'%\'"
537
+ client.query(cmd) rescue Mysql2::Error
538
+
539
+ if client
540
+ cmd = "CREATE USER \'cluster\'@\'localhost\' IDENTIFIED BY \'secret\'"
541
+ client.query(cmd)
542
+ cmd = "GRANT ALL PRIVILEGES ON *.* TO \'cluster\'@'\localhost\'"
543
+ client.query(cmd)
544
+ cmd = "CREATE USER \'cluster\'@\'%\' IDENTIFIED BY \'secret\'"
545
+ client.query(cmd)
546
+ cmd = "GRANT ALL PRIVILEGES ON *.* TO \'cluster\'@\'%\'"
547
+ client.query(cmd)
548
+ else
549
+ puts "Could not open connection to MySQL instance #{instance}."
550
+ end
551
+ rescue Mysql2::Error => e
552
+ puts e.message
553
+ puts e.backtrace
554
+ ensure
555
+ client.close if client
556
+ end
557
+
558
+ # Create the 'widgets' database.
559
+ def do_create_widgets(instance)
560
+ client = Client.open(instance)
561
+ if client
562
+ client.query("drop database if exists widgets")
563
+ client.query("create database widgets")
564
+ else
565
+ puts "Could not open connection to MySQL instance #{instance}."
566
+ end
567
+ rescue Mysql2::Error => e
568
+ puts e.message
569
+ puts e.backtrace
570
+ ensure
571
+ client.close if client
572
+ end
573
+
574
+ # This is an example template to create commands to issue queries.
575
+ def template(instance)
576
+ client = Client.open(instance)
577
+ if client
578
+ client.query("some SQL statement")
579
+ else
580
+ puts "Could not open connection to MySQL instance #{instance}."
581
+ end
582
+ rescue Mysql2::Error => e
583
+ puts e.message
584
+ puts e.backtrace
585
+ ensure
586
+ client.close if client
587
+ end
588
+
589
+ private
590
+
591
+ def run_cmd(cmd, verbose)
592
+ puts cmd if verbose
593
+ cmd += " > /dev/null 2>&1" unless verbose
594
+ output = %x[#{cmd}]
595
+ puts output if verbose
596
+ exit_code = $?.exitstatus
597
+ if exit_code == 0
598
+ puts "OK"
599
+ else
600
+ "FAIL: exit code is #{exit_code}"
601
+ end
602
+ end
603
+
604
+ # Return the process ID (pid) for an instance.
605
+ def get_mysqld_pid(instance)
606
+ server = server_for_instance(instance)
607
+ pidfile = server['pid-file']
608
+ return nil unless File.exist?(pidfile)
609
+ Integer(File.open(pidfile, &:readline).strip)
610
+ end
611
+ end
612
+ end
613
+
614
+ # STOP SLAVE
615
+ # RESET MASTER
616
+ # CHANGE MASTER TO
617
+
618
+ class RunExamples
619
+ include Repctl::Commands
620
+
621
+ def runtest
622
+
623
+ switch_master_to(3)
624
+ =begin
625
+ (1..4).each {|i| ensure_running(i)}
626
+ puts get_master_coordinates(1)
627
+ (2..4).each {|i| puts get_slave_coordinates(i)}
628
+ (1..4).each {|i| puts is_master?(i) }
629
+ puts "Master is #{find_master}"
630
+ drain_relay_log(2)
631
+ # sleep(10)
632
+ # (1..4).each {|i| crash(i)}
633
+ =end
634
+ end
635
+ end
636
+
637
+
638
+
@@ -0,0 +1,48 @@
1
+ require 'yaml'
2
+
3
+ module Repctl
4
+
5
+ module Servers
6
+
7
+ include Config
8
+
9
+ def all_servers
10
+ @servers ||= File.open(SERVER_CONFIG) { |yf| YAML::load( yf ) }
11
+ end
12
+
13
+ def all_instances
14
+ instances = []
15
+ all_servers.each do |s|
16
+ instances << s["instance"]
17
+ end
18
+ instances
19
+ end
20
+
21
+ def server_for_instance(instance)
22
+ all_servers.select {|s| s["instance"] == Integer(instance)}.shift
23
+ end
24
+
25
+ def all_live_servers
26
+ all_servers.select {|s| get_mysqld_pid(s["instance"]) }
27
+ end
28
+
29
+ def all_live_instances
30
+ all_live_servers.map {|s| s["instance"]}
31
+ end
32
+
33
+ def live?(instance)
34
+ get_mysqld_pid(instance)
35
+ end
36
+
37
+ private
38
+
39
+ # Return the process ID (pid) for an instance.
40
+ def get_mysqld_pid(instance)
41
+ server = server_for_instance(instance)
42
+ pidfile = server['pid-file']
43
+ return nil unless File.exist?(pidfile)
44
+ Integer(File.open(pidfile, &:readline).strip)
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,3 @@
1
+ module Repctl
2
+ VERSION = "0.0.1"
3
+ end
data/lib/repctl.rb ADDED
@@ -0,0 +1,9 @@
1
+ require File.expand_path('../../config/config', __FILE__)
2
+
3
+ require "repctl/version"
4
+ require "repctl/servers"
5
+ require "repctl/mysql_admin"
6
+
7
+ module Repctl
8
+ # Your code goes here...
9
+ end