Revision 45bfe9aa

View differences:

install.sh
1168 1168
            src/onedb/vcenter_one54.rb \
1169 1169
            src/onedb/sqlite2mysql.rb \
1170 1170
            src/onedb/database_schema.rb \
1171
            src/onedb/fsck"
1171
            src/onedb/fsck \
1172
            src/onedb/onedb_live.rb"
1172 1173

  
1173 1174
ONEDB_SHARED_MIGRATOR_FILES="src/onedb/shared/2.0_to_2.9.80.rb \
1174 1175
                             src/onedb/shared/2.9.80_to_2.9.85.rb \
src/onedb/onedb
46 46
$: << RUBY_LIB_LOCATION+'/onedb'
47 47

  
48 48
require 'cli/command_parser'
49
require 'optparse/time'
49 50
require 'onedb'
51
require 'onedb_live'
50 52
require 'opennebula'
51 53

  
52 54
FORCE={
......
224 226
    :format => Array
225 227
}
226 228

  
229
###############################################################################
230
# Live operation options
231
###############################################################################
232

  
233
START_TIME = {
234
    :name   => "start_time",
235
    :short  => "-s TIME",
236
    :large  => "--start TIME" ,
237
    :description => "First time to process",
238
    :format => Time
239
}
240

  
241
END_TIME = {
242
    :name   => "end_time",
243
    :short  => "-e TIME",
244
    :large  => "--end TIME" ,
245
    :description => "Last time to process",
246
    :format => Time
247
}
248

  
249
ID = {
250
    :name   => "id",
251
    :short  => "-i ID",
252
    :large  => "--id ID" ,
253
    :description => "Filter by ID",
254
    :format => Numeric
255
}
256

  
257
XPATH = {
258
    :name   => "xpath",
259
    :short  => "-x ID",
260
    :large  => "--xpath ID" ,
261
    :description => "Filter by xpath",
262
    :format => String
263
}
264

  
265
EXPR= {
266
    :name   => "expr",
267
    :short  => "-e ID",
268
    :large  => "--expr ID" ,
269
    :description => "Filter by expression (UNAME=oneadmin)",
270
    :format => String
271
}
272

  
273
DRY= {
274
    :name   => "dry",
275
    :large  => "--dry" ,
276
    :description => "Do not write in the database, output xml"
277
}
278

  
279
DELETE= {
280
    :name   => "delete",
281
    :short  => "-d",
282
    :large  => "--delete" ,
283
    :description => "Delete all matched xpaths"
284
}
227 285

  
228 286
cmd=CommandParser::CmdParser.new(ARGV) do
229 287
    description <<-EOT.unindent
......
414 472
            [-1, e.message]
415 473
        end
416 474
    end
475

  
476
    LIVE_ACTION_HELP = <<-EOT.unindent
477
        **WARNING**: This action is done while OpenNebula is running. Make
478
        a backup of the datasbase before executing.
479
    EOT
480

  
481
    ###########################################################################
482
    # Purge history
483
    ###########################################################################
484
    purge_history_desc = <<-EOT.unindent
485
        Deletes all but the last history records from non DONE VMs
486

  
487
        #{LIVE_ACTION_HELP}
488
    EOT
489

  
490
    command :'purge-history', purge_history_desc,
491
            :options => [START_TIME, END_TIME] do
492
        begin
493
            action = OneDBLive.new
494
            action.purge_history(options)
495
        rescue Exception => e
496
            puts e.message
497
            pp e.backtrace
498
            [-1, e.message]
499
        end
500

  
501
        0
502
    end
503

  
504
    ###########################################################################
505
    # Purge VMs in DONE state
506
    ###########################################################################
507
    purge_done_desc = <<-EOT.unindent
508
        Deletes all VMs in DONE state
509

  
510
        #{LIVE_ACTION_HELP}
511
    EOT
512

  
513
    command :'purge-done', purge_done_desc,
514
            :options => [START_TIME, END_TIME] do
515
        begin
516
            action = OneDBLive.new
517
            action.purge_done_vm(options)
518
        rescue Exception => e
519
            puts e.name
520
            pp e.backtrace
521
            [-1, e.message]
522
        end
523

  
524
        0 # exit code
525
    end
526

  
527
    ###########################################################################
528
    # Change value in object body
529
    ###########################################################################
530
    change_body_desc = <<-EOT.unindent
531
        Changes a value from the body of an object. The possible objects are:
532
            vm, host, vnet, image, cluster, document, group, marketplace,
533
            marketplaceapp, secgroup, template, vrouter or zone
534

  
535
        You can filter the objects to modify using one of these options:
536

  
537
            * --id: object id, example: 156
538
            * --xpath: xpath expression, example: TEMPLATE[count(NIC)>1]
539
            * --expr: xpath expression, can use operators =, !=, <, >, <= or >=
540
                examples: UNAME=oneadmin, TEMPLATE/NIC/NIC_ID>0
541

  
542
        If you want to change a value use a third parameter. In case you want
543
        to delete it use --delete option.
544

  
545
        Change the second network of VMs that belong to "user":
546

  
547
            onedb change-body vm --expr UNAME=user \\
548
                '/VM/TEMPLATE/NIC[NETWORK="service"]/NETWORK' new_network
549

  
550
        Delete cache attribute in all disks, write xml, do not modify DB:
551

  
552
            onedb change-body vm '/VM/TEMPLATE/DISK/CACHE' --delete --dry
553

  
554
        Delete cache attribute in all disks in poweroff:
555

  
556
            onedb change-body vm --expr LCM_STATE=8 \\
557
                '/VM/TEMPLATE/DISK/CACHE' --delete
558

  
559
        #{LIVE_ACTION_HELP}
560
    EOT
561

  
562
    command :'change-body', change_body_desc, :object, :xpath, [:value, nil],
563
            :options => [ID, XPATH, EXPR, DRY, DELETE] do
564
        begin
565
            action = OneDBLive.new
566
            action.change_body(args[0], args[1], args[2], options)
567
        rescue Exception => e
568
            puts e.message
569
            pp e.backtrace
570
            [-1, e.message]
571
        end
572

  
573
        0 # exit code
574
    end
417 575
end
src/onedb/onedb_live.rb
1

  
2
require 'opennebula'
3
require 'base64'
4

  
5
class OneDBLive
6
    def initialize
7
        @client = nil
8
        @system = nil
9
    end
10

  
11
    def client
12
        @client ||= OpenNebula::Client.new
13
    end
14

  
15
    def system
16
        @system ||= OpenNebula::System.new(client)
17
    end
18

  
19
    def db_escape(string)
20
        string.gsub("'", "''")
21
    end
22

  
23
    def delete_sql(table, where)
24
        "DELETE from #{table} WHERE #{where}"
25
    end
26

  
27
    def delete(table, where, federate)
28
        sql = delete_sql(table, where)
29
        db_exec(sql, "Error deleting record", federate)
30
    end
31

  
32
    def update_sql(table, values, where)
33
        str = "UPDATE #{table} SET "
34

  
35
        changes = []
36

  
37
        values.each do |key, value|
38
            change = "#{key.to_s} = "
39

  
40
            case value
41
            when String, Symbol
42
                change << "'#{db_escape(value.to_s)}'"
43
            when Numeric
44
                change << value.to_s
45
            else
46
                change << value.to_s
47
            end
48

  
49
            changes << change
50
        end
51

  
52
        str << changes.join(', ')
53
        str << " WHERE #{where}"
54

  
55
        str
56
    end
57

  
58
    def update(table, values, where, federate)
59
        sql = update_sql(table, values, where)
60
        db_exec(sql, "Error updating record", federate)
61
    end
62

  
63
    def update_body_sql(table, body, where)
64
        "UPDATE #{table} SET body = '#{db_escape(body)}' WHERE #{where}"
65
    end
66

  
67
    def update_body(table, body, where, federate)
68
        sql = update_body_sql(table, body, where)
69
        db_exec(sql, "Error updating record", federate)
70
    end
71

  
72
    def db_exec(sql, error_msg, federate = false)
73
        rc = system.sql_command(sql, federate)
74
        if OpenNebula.is_error?(rc)
75
            raise "#{error_msg}: #{rc.message}"
76
        end
77
    end
78

  
79
    def select(table, where)
80
        sql = "SELECT * FROM #{table} WHERE #{where}"
81
        res = db_query(sql, "Error querying database")
82

  
83
        element = OpenNebula::XMLElement.new(
84
            OpenNebula::XMLElement.build_xml(res, '/SQL_COMMAND'))
85

  
86
        hash = element.to_hash
87

  
88
        row = hash['SQL_COMMAND']['RESULT']['ROW'] rescue nil
89
        [row].flatten.compact
90
    end
91

  
92
    def db_query(sql, error_msg)
93
        rc = system.sql_query_command(sql)
94
        if OpenNebula.is_error?(rc)
95
            raise "#{error_msg}: #{rc.message}"
96
        end
97

  
98
        rc
99
    end
100

  
101
    def percentage_line(current, max, carriage_return = false)
102
        return_symbol = carriage_return ? "\r" : ""
103
        percentile = current.to_f / max.to_f * 100
104

  
105
        "#{current}/#{max} #{percentile.round(2)}%#{return_symbol}"
106
    end
107

  
108
    def purge_history(options = {})
109
        vmpool = OpenNebula::VirtualMachinePool.new(client)
110
        vmpool.info_all
111

  
112
        ops = {
113
            start_time: 0,
114
            end_time: Time.now
115
        }.merge(options)
116

  
117
        start_time  = ops[:start_time].to_i
118
        end_time    = ops[:end_time].to_i
119

  
120
        last_id = vmpool["/VM_POOL/VM[last()]/ID"]
121

  
122
        vmpool.each do |vm|
123
            print percentage_line(vm.id, last_id, true)
124

  
125
            time = vm["STIME"].to_i
126
            next unless time >= start_time && time < end_time
127

  
128
            # vmpool info only returns the last history record. We can check
129
            # if this VM can have more than one record using the sequence
130
            # number. If it's 0 or it does not exist we can skip the VM.
131
            # Also take tone that xpaths on VM info that comes from VMPool
132
            # or VM is different. We can not use absolute searches with
133
            # objects coming from pool.
134
            seq = vm['HISTORY_RECORDS/HISTORY/SEQ']
135
            next if !seq || seq == '0'
136

  
137
            # If the history can contain more than one record we get
138
            # all the info for two reasons:
139
            #
140
            #   * Make sure that all the info is written back
141
            #   * Refresh the information so it's less probable that the info
142
            #     was modified during this process
143
            vm.info
144

  
145
            hash = vm.to_hash
146
            val_history = hash['VM']['HISTORY_RECORDS']['HISTORY']
147

  
148
            history_num = 2
149

  
150
            if Array === val_history && val_history.size > history_num
151
                last_history = val_history.last(history_num)
152

  
153
                old_seq = []
154
                seq_num = last_history.first['SEQ']
155

  
156
                # Renumerate the sequence
157
                last_history.each_with_index do |history, index|
158
                    old_seq << history['SEQ'].to_i
159
                    history['SEQ'] = index
160
                end
161

  
162
                # Only the last history record is saved in vm_pool
163
                vm.delete_element('HISTORY_RECORDS/HISTORY')
164
                vm.add_element('HISTORY_RECORDS',
165
                               'HISTORY' => last_history.last)
166

  
167
                # Update VM body to leave only the last history record
168
                body = db_escape(vm.to_xml)
169
                update_body("vm_pool", vm.to_xml, "oid = #{vm.id}", false)
170

  
171
                # Delete any history record that does not have the same
172
                # SEQ number as the last history record
173
                delete("history", "vid = #{vm.id} and seq < #{seq_num}", false)
174

  
175
                # Get VM history
176
                history = select("history", "vid = #{vm.id}")
177

  
178
                # Renumerate sequence numbers
179
                old_seq.each_with_index do |seq, index|
180
                    row = history.find {|r| seq.to_s == r["seq"] }
181
                    body = Base64.decode64(row['body64'])
182

  
183
                    doc = Nokogiri::XML(body)
184
                    doc.xpath("/HISTORY/SEQ").first.content = index.to_s
185
                    new_body = doc.root.to_xml
186

  
187
                    update("history",
188
                           { seq: index, body: new_body },
189
                           "vid = #{vm.id} and seq = #{seq}", false)
190
                end
191
            end
192
        end
193
    end
194

  
195
    def purge_done_vm(options = {})
196
        vmpool = OpenNebula::VirtualMachinePool.new(client)
197
        vmpool.info(OpenNebula::Pool::INFO_ALL,
198
                    -1,
199
                    -1,
200
                    OpenNebula::VirtualMachine::VM_STATE.index('DONE'))
201

  
202
        ops = {
203
            start_time: 0,
204
            end_time: Time.now
205
        }.merge(options)
206

  
207
        start_time  = ops[:start_time].to_i
208
        end_time    = ops[:end_time].to_i
209

  
210
        last_id = vmpool["/VM_POOL/VM[last()]/ID"]
211

  
212
        vmpool.each do |vm|
213
            print percentage_line(vm.id, last_id, true)
214

  
215
            time = vm["ETIME"].to_i
216
            next unless time >= start_time && time < end_time
217

  
218
            delete("vm_pool", "oid = #{vm.id}", false)
219
            delete("history", "vid = #{vm.id}", false)
220
        end
221
    end
222

  
223
    def check_expr(object, expr)
224
        reg = /^(?<xpath>.+?)(?<operator>=|!=|>=|<=|>|<)(?<value>.*?)$/
225
        parsed = expr.match(reg)
226

  
227
        raise "Expression malformed: '#{expr}'" unless parsed
228

  
229
        val = object[parsed[:xpath]]
230
        return false if !val
231

  
232
        p_val = parsed[:value].strip
233
        val.strip!
234

  
235
        res = false
236

  
237
        res = case parsed[:operator]
238
        when '='
239
            val == p_val
240
        when '!='
241
            val != p_val
242
        when '<'
243
            val.to_i < p_val.to_i
244
        when '>'
245
            val.to_i > p_val.to_i
246
        when '<='
247
            val.to_i <= p_val.to_i
248
        when '>='
249
            val.to_i >= p_val.to_i
250
        end
251

  
252
        res
253
    end
254

  
255
    def change_body(object, xpath, value, options = {})
256
        case (object||'').downcase.strip.to_sym
257
        when :vm
258
            table = 'vm_pool'
259
            object = OpenNebula::VirtualMachinePool.new(client)
260
            federate = false
261

  
262
        when :host
263
            table = 'host_pool'
264
            object = OpenNebula::HostPool.new(client)
265
            federate = false
266

  
267
        when :vnet
268
            table = 'network_pool'
269
            object = OpenNebula::VirtualNetworkPool.new(client)
270
            federate = false
271

  
272
        when :image
273
            table = 'image_pool'
274
            object = OpenNebula::ImagePool.new(client)
275
            federate = false
276

  
277
        when :cluster
278
            table = 'cluster_pool'
279
            object = OpenNebula::ClusterPool.new(client)
280
            federate = false
281

  
282
        when :document
283
            table = 'document_pool'
284
            object = OpenNebula::DocumentPool.new(client)
285
            federate = false
286

  
287
        when :group
288
            table = 'group_pool'
289
            object = OpenNebula::GroupPool.new(client)
290
            federate = true
291

  
292
        when :marketplace
293
            table = 'marketplace_pool'
294
            object = OpenNebula::MarketPlacePool.new(client)
295
            federate = true
296

  
297
        when :marketplaceapp
298
            table = 'marketplaceapp_pool'
299
            object = OpenNebula::MarketPlaceAppPool.new(client)
300
            federate = true
301

  
302
        when :secgroup
303
            table = 'secgroup_pool'
304
            object = OpenNebula::SecurityGroupPool.new(client)
305
            federate = false
306

  
307
        when :template
308
            table = 'template_pool'
309
            object = OpenNebula::TemplatePool.new(client)
310
            federate = false
311

  
312
        when :vrouter
313
            table = 'vrouter_pool'
314
            object = OpenNebula::VirtualRouterPool.new(client)
315
            federate = false
316

  
317
        when :zone
318
            table = 'zone_pool'
319
            object = OpenNebula::ZonePool.new(client)
320
            federate = true
321

  
322
        else
323
            raise "Object type '#{object}' not supported"
324
        end
325

  
326
        if !value && !options[:delete]
327
            raise "A value or --delete should specified"
328
        end
329

  
330
        object.info
331

  
332
        object.each do |o|
333
            if options[:id]
334
                next unless o.id.to_s.strip == options[:id].to_s
335
            elsif options[:xpath]
336
                next unless o[options[:xpath]]
337
            elsif options[:expr]
338
                next unless check_expr(o, options[:expr])
339
            end
340

  
341
            o.info
342
            doc = Nokogiri::XML(o.to_xml, nil, NOKOGIRI_ENCODING) do |c|
343
                c.default_xml.noblanks
344
            end
345

  
346
            doc.xpath(xpath).each do |e|
347
                if options[:delete]
348
                    e.remove
349
                else
350
                    e.content = value
351
                end
352
            end
353

  
354
            xml = doc.root.to_xml
355

  
356
            if options[:dry]
357
                puts xml
358
            else
359
                update_body(table, xml, "oid = #{o.id}", federate)
360
            end
361
        end
362
    end
363
end
364

  

Also available in: Unified diff