Statistics
| Branch: | Tag: | Revision:

one / src / onedb / onedb_backend.rb @ 25b48b1e

History | View | Annotate | Download (12.2 KB)

1
# -------------------------------------------------------------------------- #
2
# Copyright 2002-2017, OpenNebula Project, OpenNebula Systems                #
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 'time'
18
require 'rubygems'
19
require 'cgi'
20
require 'database_schema'
21
require 'open3'
22

    
23
begin
24
    require 'sequel'
25
rescue LoadError
26
    STDERR.puts "Ruby gem sequel is needed for this operation:"
27
    STDERR.puts "  $ sudo gem install sequel"
28
    exit -1
29
end
30

    
31
class OneDBBacKEnd
32
    FEDERATED_TABLES = %w(group_pool user_pool acl zone_pool vdc_pool
33
                          marketplace_pool marketplaceapp_pool)
34

    
35
    def read_db_version
36
        connect_db
37

    
38
        ret = {}
39

    
40
        begin
41
            ret[:version]   = "2.0"
42
            ret[:timestamp] = 0
43
            ret[:comment]   = ""
44

    
45
            @db.fetch("SELECT version, timestamp, comment FROM db_versioning " +
46
                      "WHERE oid=(SELECT MAX(oid) FROM db_versioning)") do |row|
47
                ret[:version]   = row[:version]
48
                ret[:timestamp] = row[:timestamp]
49
                ret[:comment]   = row[:comment]
50
            end
51

    
52
            ret[:local_version]   = ret[:version]
53
            ret[:local_timestamp] = ret[:timestamp]
54
            ret[:local_comment]   = ret[:comment]
55
            ret[:is_slave]        = false
56

    
57
            begin
58
               @db.fetch("SELECT version, timestamp, comment, is_slave FROM "+
59
                        "local_db_versioning WHERE oid=(SELECT MAX(oid) "+
60
                        "FROM local_db_versioning)") do |row|
61
                    ret[:local_version]   = row[:version]
62
                    ret[:local_timestamp] = row[:timestamp]
63
                    ret[:local_comment]   = row[:comment]
64
                    ret[:is_slave]        = row[:is_slave]
65
               end
66
            rescue Exception => e
67
                if e.class == Sequel::DatabaseConnectionError
68
                    raise e
69
                end
70
            end
71

    
72
            return ret
73

    
74
        rescue Exception => e
75
            if e.class == Sequel::DatabaseConnectionError
76
                raise e
77
            elsif !db_exists?
78
                # If the DB doesn't have db_version table, it means it is empty or a 2.x
79
                raise "Database schema does not look to be created by " <<
80
                      "OpenNebula: table user_pool is missing or empty."
81
            end
82

    
83
            begin
84
                # Table image_pool is present only in 2.X DBs
85
                @db.fetch("SELECT * FROM image_pool") { |row| }
86
            rescue
87
                raise "Database schema looks to be created by OpenNebula 1.X." <<
88
                      "This tool only works with databases created by 2.X versions."
89
            end
90

    
91
            comment = "Could not read any previous db_versioning data, " <<
92
                      "assuming it is an OpenNebula 2.0 or 2.2 DB."
93

    
94
            return ret
95
        end
96
    end
97

    
98
    def history
99
        connect_db
100

    
101
        begin
102
            query = "SELECT version, timestamp, comment FROM db_versioning"
103
            @db.fetch(query) do |row|
104
                puts "Version:   #{row[:version]}"
105

    
106
                time = Time.at(row[:timestamp])
107
                puts "Timestamp: #{time.strftime("%m/%d %H:%M:%S")}"
108

    
109
                puts "Comment:   #{row[:comment]}"
110

    
111
                puts ""
112
            end
113
        rescue Exception => e
114
            raise "No version records found. Error message: " + e.message
115
        end
116
    end
117

    
118
    def update_db_version(version)
119
        comment = "Database migrated from #{version} to #{db_version}"+
120
                  " (#{one_version}) by onedb command."
121

    
122
        max_oid = nil
123
        @db.fetch("SELECT MAX(oid) FROM db_versioning") do |row|
124
            max_oid = row[:"MAX(oid)"].to_i
125
        end
126

    
127
        max_oid = 0 if max_oid.nil?
128

    
129
        query =
130
        @db.run(
131
            "INSERT INTO db_versioning (oid, version, timestamp, comment) "<<
132
            "VALUES ("                                                     <<
133
                "#{max_oid+1}, "                                           <<
134
                "'#{db_version}', "                                        <<
135
                "#{Time.new.to_i}, "                                       <<
136
                "'#{comment}')"
137
        )
138

    
139
        puts comment
140
    end
141

    
142
    def update_local_db_version(version)
143
        comment = "Database migrated from #{version} to #{db_version}"+
144
                  " (#{one_version}) by onedb command."
145

    
146
        max_oid = nil
147
        @db.fetch("SELECT MAX(oid) FROM local_db_versioning") do |row|
148
            max_oid = row[:"MAX(oid)"].to_i
149
        end
150

    
151
        max_oid = 0 if max_oid.nil?
152

    
153
        is_slave = 0
154

    
155
        @db.fetch("SELECT is_slave FROM local_db_versioning "<<
156
                  "WHERE oid=#{max_oid}") do |row|
157
            is_slave = row[:is_slave] ? 1 : 0
158
        end
159

    
160
        @db.run(
161
            "INSERT INTO local_db_versioning (oid, version, timestamp, comment, is_slave) "<<
162
            "VALUES ("                                                     <<
163
                "#{max_oid+1}, "                                           <<
164
                "'#{db_version}', "                                        <<
165
                "#{Time.new.to_i}, "                                       <<
166
                "'#{comment}',"                                            <<
167
                "#{is_slave})"
168
        )
169

    
170
        puts comment
171
    end
172

    
173
    def db()
174
        return @db
175
    end
176

    
177
    private
178

    
179
    def db_exists?
180
        begin
181
            found = false
182

    
183
            # User with ID 0 (oneadmin) always exists
184
            @db.fetch("SELECT * FROM user_pool WHERE oid=0") { |row|
185
                found = true
186
            }
187
        rescue
188
        end
189

    
190
        return found
191
    end
192

    
193
    def init_log_time()
194
        @block_n = 0
195
        @time0 = Time.now
196
    end
197

    
198
    def log_time()
199
        if LOG_TIME
200
            @time1 = Time.now
201
            puts "    > #{db_version} Time for block #{@block_n}: #{"%0.02f" % (@time1 - @time0).to_s}s"
202
            @time0 = Time.now
203
            @block_n += 1
204
        end
205
    end
206
end
207

    
208
class BackEndMySQL < OneDBBacKEnd
209
    def initialize(opts={})
210
        @server  = opts[:server]
211
        @port    = opts[:port]
212
        @user    = opts[:user]
213
        @passwd  = opts[:passwd]
214
        @db_name = opts[:db_name]
215

    
216
        # Check for errors:
217
        error   = false
218

    
219
        (error = true; missing = "USER"  )  if @user    == nil
220
        (error = true; missing = "DBNAME")  if @db_name == nil
221

    
222
        if error
223
            raise "MySQL option #{missing} is needed"
224
        end
225

    
226
        # Check for defaults:
227
        @server = "localhost"   if @server.nil?
228
        @port   = 0             if @port.nil?
229

    
230
        # Clean leading and trailing quotes, if any
231
        @server  = @server [1..-2] if @server [0] == ?"
232
        @port    = @port   [1..-2] if @port   [0] == ?"
233
        @user    = @user   [1..-2] if @user   [0] == ?"
234
        @passwd  = @passwd [1..-2] if @passwd [0] == ?"
235
        @db_name = @db_name[1..-2] if @db_name[0] == ?"
236
    end
237

    
238
    def bck_file(federated = false)
239
        t = Time.now
240

    
241
        bck_name = "#{VAR_LOCATION}/mysql_#{@server}_#{@db_name}_"
242

    
243
        bck_name << "federated_" if federated
244

    
245
        bck_name << "#{t.year}-#{t.month}-#{t.day}_"
246
        bck_name << "#{t.hour}:#{t.min}:#{t.sec}.sql"
247

    
248
        bck_name
249
    end
250

    
251
    def backup(bck_file, federated = false)
252
        cmd = "mysqldump -u #{@user} -p'#{@passwd}' -h #{@server} " <<
253
              "-P #{@port} --add-drop-table #{@db_name} "
254

    
255
        cmd << FEDERATED_TABLES.join(" ") if federated
256

    
257
        cmd << " > #{bck_file}"
258

    
259
        rc = system(cmd)
260

    
261
        if !rc
262
            raise "Unknown error running '#{cmd}'"
263
        end
264

    
265
        if federated
266
            cmd = "mysqldump -u #{@user} -p'#{@passwd}' -h #{@server} " <<
267
                  "-P #{@port} #{@db_name} logdb --where=\"fed_index!=-1\" "<<
268
                  " >> #{bck_file}"
269

    
270
            rc = system(cmd)
271

    
272
            if !rc
273
                raise "Unknown error running '#{cmd}'"
274
            end
275
        end
276

    
277
        puts "MySQL dump stored in #{bck_file}"
278
        puts "Use 'onedb restore' or restore the DB using the mysql command:"
279
        puts "mysql -u user -h server -P port db_name < backup_file"
280
        puts
281
    end
282

    
283
    def restore(bck_file, force=nil, federated=false)
284
        connect_db
285

    
286
        if !federated && !force && db_exists?
287
            raise "MySQL database #{@db_name} at #{@server} exists," <<
288
                  " use -f to overwrite."
289
        end
290

    
291
        rc = system("mysql -u #{@user} -p'#{@passwd}' -h #{@server} -P #{@port} #{@db_name} < #{bck_file}")
292
        if !rc
293
            raise "Error while restoring MySQL DB #{@db_name} at #{@server}."
294
        end
295

    
296
        puts "MySQL DB #{@db_name} at #{@server} restored."
297
    end
298

    
299
    private
300

    
301
    def connect_db
302
        passwd = CGI.escape(@passwd)
303

    
304
        endpoint = "mysql2://#{@user}:#{passwd}@#{@server}:#{@port}/#{@db_name}"
305

    
306
        begin
307
            @db = Sequel.connect(endpoint)
308
        rescue Exception => e
309
            raise "Error connecting to DB: " + e.message
310
        end
311
    end
312
end
313

    
314
class BackEndSQLite < OneDBBacKEnd
315
    require 'fileutils'
316

    
317
    def initialize(file)
318
        if !file.nil?
319
            @sqlite_file = file
320
        else
321
            raise "SQLite database path not supplied."
322
        end
323
    end
324

    
325
    def bck_file(federated = false)
326
        t = Time.now
327
        bck_name = "#{VAR_LOCATION}/one.db_"
328

    
329
        bck_name << "federated_" if federated
330

    
331
        bck_name << "#{t.year}-#{t.month}-#{t.day}"
332
        bck_name << "_#{t.hour}:#{t.min}:#{t.sec}.bck"
333

    
334
        bck_name
335
    end
336

    
337
    def backup(bck_file, federated = false)
338
        if federated
339
            puts "Sqlite database backup of federated tables stored in #{bck_file}"
340

    
341
            File.open(bck_file, "w") do |f|
342
                f.puts "-- FEDERATED"
343
                FEDERATED_TABLES.each do |table|
344
                    f.puts "DROP TABLE IF EXISTS \"#{table}\";"
345
                end
346
            end
347

    
348
            FEDERATED_TABLES.each do |table|
349
                Open3.popen3("sqlite3 #{@sqlite_file} '.dump #{table}' >> #{bck_file}") do |i,o,e,t|
350
                    stdout = o.read
351
                    if !stdout.empty?
352
                        puts stdout
353
                    end
354

    
355
                    stderr = e.read
356
                    if !stderr.empty?
357
                        stderr.lines.each do |line|
358
                            STDERR.puts line unless line.match(/^-- Loading/)
359
                        end
360
                    end
361
                end
362

    
363
            end
364
        else
365
            puts "Sqlite database backup stored in #{bck_file}"
366
            system("sqlite3 #{@sqlite_file} .dump > #{bck_file}")
367
        end
368

    
369
        puts "Use 'onedb restore' to restore the DB."
370
    end
371

    
372
    def restore(bck_file, force=nil, federated=false)
373
        if !federated
374
            if File.exists?(@sqlite_file) && !force
375
                raise "File #{@sqlite_file} exists, use -f to overwrite."
376
            end
377
        end
378

    
379
        system("sqlite3 #{@sqlite_file} < #{bck_file}")
380
        puts "Sqlite database backup restored in #{@sqlite_file}"
381
    end
382

    
383
    private
384

    
385
    def connect_db
386
        if !File.exists?(@sqlite_file)
387
            raise "File #{@sqlite_file} doesn't exist"
388
        end
389

    
390
        begin
391
            @db = Sequel.sqlite(@sqlite_file)
392
            @db.integer_booleans = true
393
        rescue Exception => e
394
            raise "Error connecting to DB: " + e.message
395
        end
396
    end
397
end