Statistics
| Branch: | Tag: | Revision:

one / src / onedb / onedb.rb @ d12c81e6

History | View | Annotate | Download (15.5 KB)

1
# -------------------------------------------------------------------------- #
2
# Copyright 2002-2015, OpenNebula Project (OpenNebula.org), C12G Labs        #
3
#                                                                            #
4
# Licensed under the Apache License, Version 2.0 (the "License"); you may    #
5
# not use this file except in compliance with the License. You may obtain    #
6
# a copy of the License at                                                   #
7
#                                                                            #
8
# http://www.apache.org/licenses/LICENSE-2.0                                 #
9
#                                                                            #
10
# Unless required by applicable law or agreed to in writing, software        #
11
# distributed under the License is distributed on an "AS IS" BASIS,          #
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   #
13
# See the License for the specific language governing permissions and        #
14
# limitations under the License.                                             #
15
#--------------------------------------------------------------------------- #
16

    
17
require 'onedb_backend'
18

    
19
# If set to true, extra verbose time log will be printed for each migrator
20
LOG_TIME = false
21

    
22
class OneDB
23
    def initialize(ops)
24
        if ops[:backend] == :sqlite
25
            begin
26
                require 'sqlite3'
27
            rescue LoadError
28
                STDERR.puts "Ruby gem sqlite3 is needed for this operation:"
29
                STDERR.puts "  $ sudo gem install sqlite3"
30
                exit -1
31
            end
32

    
33
            @backend = BackEndSQLite.new(ops[:sqlite])
34
        elsif ops[:backend] == :mysql
35
            begin
36
                require 'mysql'
37
            rescue LoadError
38
                STDERR.puts "Ruby gem mysql is needed for this operation:"
39
                STDERR.puts "  $ sudo gem install mysql"
40
                exit -1
41
            end
42

    
43
            passwd = ops[:passwd]
44
            if !passwd
45
                passwd = get_password
46
            end
47

    
48
            @backend = BackEndMySQL.new(
49
                :server  => ops[:server],
50
                :port    => ops[:port],
51
                :user    => ops[:user],
52
                :passwd  => passwd,
53
                :db_name => ops[:db_name]
54
            )
55
        else
56
            raise "You need to specify the SQLite or MySQL connection options."
57
        end
58
    end
59

    
60
    def get_password(question="MySQL Password: ")
61
        # Hide input characters
62
        `stty -echo`
63
        print question
64
        passwd = STDIN.gets.strip
65
        `stty echo`
66
        puts ""
67

    
68
        return passwd
69
    end
70

    
71
    def backup(bck_file, ops, backend=@backend)
72
        bck_file = backend.bck_file if bck_file.nil?
73

    
74
        if !ops[:force] && File.exists?(bck_file)
75
            raise "File #{bck_file} exists, backup aborted. Use -f " <<
76
                  "to overwrite."
77
        end
78

    
79
        backend.backup(bck_file)
80
        return 0
81
    end
82

    
83
    def restore(bck_file, ops, backend=@backend)
84
        if !File.exists?(bck_file)
85
            raise "File #{bck_file} doesn't exist, backup restoration aborted."
86
        end
87

    
88
        one_not_running
89

    
90
        backend.restore(bck_file, ops[:force])
91
        return 0
92
    end
93

    
94
    def version(ops)
95
        ret = @backend.read_db_version
96

    
97
        if(ops[:verbose])
98
            puts "Shared tables version:   #{ret[:version]}"
99

    
100
            time = ret[:version] == "2.0" ? Time.now : Time.at(ret[:timestamp])
101
            puts "Timestamp: #{time.strftime("%m/%d %H:%M:%S")}"
102
            puts "Comment:   #{ret[:comment]}"
103

    
104
            if ret[:local_version]
105
                puts
106
                puts "Local tables version:    #{ret[:local_version]}"
107

    
108
                time = Time.at(ret[:local_timestamp])
109
                puts "Timestamp: #{time.strftime("%m/%d %H:%M:%S")}"
110
                puts "Comment:   #{ret[:local_comment]}"
111

    
112
                if ret[:is_slave]
113
                    puts
114
                    puts "This database is a federation slave"
115
                end
116
            end
117

    
118
        else
119
            puts "Shared: #{ret[:version]}"
120
            puts "Local:  #{ret[:local_version]}"
121
        end
122

    
123
        return 0
124
    end
125

    
126
    def history
127
        @backend.history
128
        return 0
129
    end
130

    
131
    # max_version is ignored for now, as this is the first onedb release.
132
    # May be used in next releases
133
    def upgrade(max_version, ops)
134
        one_not_running()
135

    
136
        db_version = @backend.read_db_version
137

    
138
        if ops[:verbose]
139
            pretty_print_db_version(db_version)
140

    
141
            puts ""
142
        end
143

    
144
        ops[:backup] = @backend.bck_file if ops[:backup].nil?
145

    
146
        backup(ops[:backup], ops)
147

    
148
        begin
149
            timea = Time.now
150

    
151
            # Upgrade shared (federation) tables, only for standalone and master
152
            if !db_version[:is_slave]
153
                puts
154
                puts ">>> Running migrators for shared tables"
155

    
156
                dir_prefix = "#{RUBY_LIB_LOCATION}/onedb/shared"
157

    
158
                result = apply_migrators(dir_prefix, db_version[:version], ops)
159

    
160
                # Modify db_versioning table
161
                if result != nil
162
                    @backend.update_db_version(db_version[:version])
163
                else
164
                    puts "Database already uses version #{db_version[:version]}"
165
                end
166
            end
167

    
168
            db_version = @backend.read_db_version
169

    
170
            # Upgrade local tables, for standalone, master, and slave
171

    
172
            puts
173
            puts ">>> Running migrators for local tables"
174

    
175
            dir_prefix = "#{RUBY_LIB_LOCATION}/onedb/local"
176

    
177
            result = apply_migrators(dir_prefix, db_version[:local_version], ops)
178

    
179
            # Modify db_versioning table
180
            if result != nil
181
                @backend.update_local_db_version(db_version[:local_version])
182
            else
183
                puts "Database already uses version #{db_version[:local_version]}"
184
            end
185

    
186
            timeb = Time.now
187

    
188
            puts
189
            puts "Total time: #{"%0.02f" % (timeb - timea).to_s}s" if ops[:verbose]
190

    
191
            return 0
192

    
193
        rescue Exception => e
194
            puts
195
            puts e.message
196
            puts e.backtrace.join("\n")
197
            puts
198

    
199
            puts
200
            puts "The database will be restored"
201

    
202
            ops[:force] = true
203

    
204
            restore(ops[:backup], ops)
205

    
206
            return -1
207
        end
208
    end
209

    
210
    def apply_migrators(prefix, db_version, ops)
211
        result = nil
212
        i = 0
213

    
214
        matches = Dir.glob("#{prefix}/#{db_version}_to_*.rb")
215

    
216
        while ( matches.size > 0 )
217
            if ( matches.size > 1 )
218
                raise "There are more than one file that match \
219
                        \"#{prefix}/#{db_version}_to_*.rb\""
220
            end
221

    
222
            file = matches[0]
223

    
224
            puts "  > Running migrator #{file}" if ops[:verbose]
225

    
226
            time0 = Time.now
227

    
228
            load(file)
229
            @backend.extend Migrator
230
            result = @backend.up
231

    
232
            time1 = Time.now
233

    
234
            if !result
235
                raise "Error while upgrading from #{db_version} to " <<
236
                      " #{@backend.db_version}"
237
            end
238

    
239
            puts "  > Done in #{"%0.02f" % (time1 - time0).to_s}s" if ops[:verbose]
240
            puts "" if ops[:verbose]
241

    
242
            matches = Dir.glob(
243
                "#{prefix}/#{@backend.db_version}_to_*.rb")
244
        end
245

    
246
        return result
247
    end
248

    
249
    def fsck(ops)
250
        ret = @backend.read_db_version
251

    
252
        if ops[:verbose]
253
            pretty_print_db_version(ret)
254
            puts ""
255
        end
256

    
257
        file = "#{RUBY_LIB_LOCATION}/onedb/fsck.rb"
258

    
259
        if File.exists? file
260

    
261
            one_not_running()
262

    
263
            load(file)
264
            @backend.extend OneDBFsck
265

    
266
            @backend.check_db_version()
267

    
268
            ops[:backup] = @backend.bck_file if ops[:backup].nil?
269

    
270
            # FSCK will be executed, make DB backup
271
            backup(ops[:backup], ops)
272

    
273
            begin
274
                puts "  > Running fsck" if ops[:verbose]
275

    
276
                time0 = Time.now
277

    
278
                result = @backend.fsck
279

    
280
                if !result
281
                    raise "Error running fsck version #{ret[:version]}"
282
                end
283

    
284
                puts "  > Done" if ops[:verbose]
285
                puts "" if ops[:verbose]
286

    
287
                time1 = Time.now
288

    
289
                puts "  > Total time: #{"%0.02f" % (time1 - time0).to_s}s" if ops[:verbose]
290

    
291
                return 0
292
            rescue Exception => e
293
                puts
294
                puts e.message
295
                puts e.backtrace.join("\n")
296
                puts
297

    
298
                puts "Error running fsck version #{ret[:version]}"
299
                puts "The database will be restored"
300

    
301
                ops[:force] = true
302

    
303
                restore(ops[:backup], ops)
304

    
305
                return -1
306
            end
307
        else
308
            raise "No fsck file found in #{RUBY_LIB_LOCATION}/onedb/fsck.rb"
309
        end
310
    end
311

    
312
    def import_slave(ops)
313
        if ops[:backend] == :sqlite
314
            raise "Master DB must be MySQL"
315
        end
316

    
317
        passwd = ops[:slave_passwd]
318
        if !passwd
319
            passwd = get_password("Slave MySQL Password: ")
320
        end
321

    
322
        slave_backend = BackEndMySQL.new(
323
            :server  => ops[:slave_server],
324
            :port    => ops[:slave_port],
325
            :user    => ops[:slave_user],
326
            :passwd  => passwd,
327
            :db_name => ops[:slave_db_name]
328
        )
329

    
330
        db_version = @backend.read_db_version
331

    
332
        slave_db_version = slave_backend.read_db_version
333

    
334
        if ops[:verbose]
335
            puts "Master database information:"
336
            pretty_print_db_version(db_version)
337
            puts ""
338
            puts ""
339
            puts "Slave database information:"
340
            pretty_print_db_version(slave_db_version)
341
            puts ""
342
        end
343

    
344
        file = "#{RUBY_LIB_LOCATION}/onedb/import_slave.rb"
345

    
346
        if File.exists? file
347

    
348
            one_not_running()
349

    
350
            load(file)
351
            @backend.extend OneDBImportSlave
352

    
353
            @backend.check_db_version(db_version, slave_db_version)
354

    
355
            puts <<-EOT
356
Before running this tool, it is required to create a new Zone in the
357
Master OpenNebula.
358
Please enter the Zone ID that you created to represent the new Slave OpenNebula:
359
EOT
360

    
361
            input = ""
362
            while ( input.to_i.to_s != input ) do
363
                print "Zone ID: "
364
                input = gets.chomp.strip
365
            end
366

    
367
            zone_id = input.to_i
368
            puts
369

    
370
            puts <<-EOT
371
The import process will move the users from the slave OpeNenbula to the master
372
OpenNebula. In case of conflict, it can merge users with the same name.
373
For example:
374
+----------+-------------++------------+---------------+
375
| Master   | Slave       || With merge | Without merge |
376
+----------+-------------++------------+---------------+
377
| 5, alice | 2, alice    || 5, alice   | 5, alice      |
378
| 6, bob   | 5, bob      || 6, bob     | 6, bob        |
379
|          |             ||            | 7, alice-1    |
380
|          |             ||            | 8, bob-1      |
381
+----------+-------------++------------+---------------+
382

383
In any case, the ownership of existing resources and group membership
384
is preserved.
385

386
            EOT
387

    
388
            input = ""
389
            while !( ["Y", "N"].include?(input) ) do
390
                print "Do you want to merge USERS (Y/N): "
391
                input = gets.chomp.upcase
392
            end
393

    
394
            merge_users = input == "Y"
395
            puts
396

    
397
            input = ""
398
            while !( ["Y", "N"].include?(input) ) do
399
                print "Do you want to merge GROUPS (Y/N): "
400
                input = gets.chomp.upcase
401
            end
402

    
403
            merge_groups = input == "Y"
404
            puts
405

    
406
            input = ""
407
            while !( ["Y", "N"].include?(input) ) do
408
                print "Do you want to merge VDCS (Y/N): "
409
                input = gets.chomp.upcase
410
            end
411

    
412
            merge_vdcs = input == "Y"
413

    
414
            ops[:backup] = @backend.bck_file if ops[:backup].nil?
415
            ops[:"slave-backup"] = slave_backend.bck_file if ops[:"slave-backup"].nil?
416

    
417
            # Import will be executed, make DB backup
418
            backup(ops[:backup], ops)
419
            backup(ops[:"slave-backup"], ops, slave_backend)
420

    
421
            begin
422
                puts "  > Running slave import" if ops[:verbose]
423

    
424
                result = @backend.import_slave(slave_backend, merge_users,
425
                    merge_groups, merge_vdcs, zone_id)
426

    
427
                if !result
428
                    raise "Error running slave import"
429
                end
430

    
431
                puts "  > Done" if ops[:verbose]
432
                puts "" if ops[:verbose]
433

    
434
                return 0
435
            rescue Exception => e
436
                puts
437
                puts e.message
438
                puts e.backtrace.join("\n")
439
                puts
440

    
441
                puts "Error running slave import"
442
                puts "The databases will be restored"
443

    
444
                ops[:force] = true
445

    
446
                restore(ops[:backup], ops)
447
                restore(ops[:"slave-backup"], ops, slave_backend)
448

    
449
                return -1
450
            end
451
        else
452
            raise "No slave import file found in #{RUBY_LIB_LOCATION}/onedb/import_slave.rb"
453
        end
454
    end
455

    
456
    def patch(file, ops)
457
        ret = @backend.read_db_version
458

    
459
        if ops[:verbose]
460
            pretty_print_db_version(ret)
461
            puts ""
462
        end
463

    
464
        if File.exists? file
465

    
466
            load(file)
467
            @backend.extend OneDBPatch
468

    
469
            if (!@backend.is_hot_patch(ops))
470
                one_not_running()
471
            end
472

    
473
            @backend.check_db_version(ops)
474

    
475
            ops[:backup] = @backend.bck_file if ops[:backup].nil?
476

    
477
            if (!@backend.is_hot_patch(ops))
478
                backup(ops[:backup], ops)
479
            end
480

    
481
            begin
482
                puts "  > Running patch #{file}" if ops[:verbose]
483

    
484
                time0 = Time.now
485

    
486
                result = @backend.patch(ops)
487

    
488
                if !result
489
                    raise "Error running patch #{file}"
490
                end
491

    
492
                puts "  > Done" if ops[:verbose]
493
                puts "" if ops[:verbose]
494

    
495
                time1 = Time.now
496

    
497
                puts "  > Total time: #{"%0.02f" % (time1 - time0).to_s}s" if ops[:verbose]
498

    
499
                return 0
500
            rescue Exception => e
501
                puts
502
                puts e.message
503
                puts e.backtrace.join("\n")
504
                puts
505

    
506
                puts "Error running patch #{file}"
507
                if (!@backend.is_hot_patch(ops))
508
                    puts "The database will be restored"
509

    
510
                    ops[:force] = true
511

    
512
                    restore(ops[:backup], ops)
513
                end
514

    
515
                return -1
516
            end
517
        else
518
            raise "File was not found: #{file}"
519
        end
520
    end
521

    
522
    private
523

    
524
    def one_not_running()
525
        if File.exists?(LOCK_FILE)
526
            raise "First stop OpenNebula. Lock file found: #{LOCK_FILE}"
527
        end
528

    
529
        client = OpenNebula::Client.new
530
        rc = client.get_version
531
        if !OpenNebula.is_error?(rc)
532
            raise "OpenNebula found listening on '#{client.one_endpoint}'"
533
        end
534
    end
535

    
536
    def pretty_print_db_version(db_version)
537
        puts "Version read:"
538
        puts "Shared tables #{db_version[:version]} : #{db_version[:comment]}"
539

    
540
        if db_version[:local_version]
541
            puts "Local tables  #{db_version[:local_version]} : #{db_version[:local_comment]}"
542
        end
543

    
544
        if db_version[:is_slave]
545
            puts
546
            puts "This database is a federation slave"
547
        end
548
    end
549
end