#!/usr/bin/env ruby

# -------------------------------------------------------------------------- #
# Copyright 2002-2017, OpenNebula Project, OpenNebula Systems                #
#                                                                            #
# Licensed under the Apache License, Version 2.0 (the "License"); you may    #
# not use this file except in compliance with the License. You may obtain    #
# a copy of the License at                                                   #
#                                                                            #
# http://www.apache.org/licenses/LICENSE-2.0                                 #
#                                                                            #
# Unless required by applicable law or agreed to in writing, software        #
# distributed under the License is distributed on an "AS IS" BASIS,          #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   #
# See the License for the specific language governing permissions and        #
# limitations under the License.                                             #
#--------------------------------------------------------------------------- #

nk_encoding = nil

if RUBY_VERSION =~ /^1.9/
    Encoding.default_external = Encoding::UTF_8
    Encoding.default_internal = Encoding::UTF_8
    nk_encoding = "UTF-8"
end

NOKOGIRI_ENCODING = nk_encoding

ONE_LOCATION = ENV["ONE_LOCATION"]

if !ONE_LOCATION
    LIB_LOCATION      = "/usr/lib/one"
    RUBY_LIB_LOCATION = LIB_LOCATION + "/ruby"
    VAR_LOCATION      = "/var/lib/one"
    ETC_LOCATION      = "/etc/one"
    LOCK_FILE         = "/var/lock/one/one"
else
    LIB_LOCATION      = ONE_LOCATION + "/lib"
    RUBY_LIB_LOCATION = LIB_LOCATION + "/ruby"
    VAR_LOCATION      = ONE_LOCATION + "/var"
    ETC_LOCATION      = ONE_LOCATION + "/etc"
    LOCK_FILE         = VAR_LOCATION + "/.lock"
end

$: << RUBY_LIB_LOCATION
$: << RUBY_LIB_LOCATION+'/onedb'

require 'cli/command_parser'
require 'optparse/time'
require 'onedb'
require 'onedb_live'
require 'opennebula'

FORCE={
    :name => "force",
    :short => "-f",
    :large => "--force",
    :description => "Forces the backup even if the DB exists"
}

BACKUP={
    :name => "backup",
    :short => "-b file",
    :large => "--backup file",
    :description => "Use this file to store SQL dump",
    :format => String
}

FEDERATED = {
    :name        => "federated",
    :large       => "--federated",
    :description => "Limit backup/restore to federated tables"
}

###############################################################################
# SQLite options
###############################################################################
SQLITE={
    :name => "sqlite",
    :short => "-s file",
    :large => "--sqlite file",
    :format => String,
    :description => "SQLite DB file",
    :proc => lambda { |o, options|
        options[:backend] = :sqlite
        options[:sqlite]  = o
    }
}

###############################################################################
# MySQL options
###############################################################################
SERVER={
    :name => "server",
    :short => "-S host",
    :large => "--server host",
    :format => String,
    :description => "MySQL server hostname or IP. Defaults to localhost",
    :proc => lambda { |o, options|
        options[:backend] = :mysql
        options[:server]  = o
    }
}

PORT={
    :name => "port",
    :short => "-P port",
    :large => "--port port",
    :format => String,
    :description => "MySQL server port. Defaults to 3306",
    :proc => lambda { |o, options|
        options[:backend] = :mysql
        options[:port]  = o
    }
}

USERNAME={
    :name => "username",
    :short => "-u user",
    :large => "--username user",
    :format => String,
    :description => "MySQL username",
    :proc => lambda { |o, options|
        options[:backend] = :mysql
        options[:user]    = o
    }
}

PASSWORD={
    :name => "password",
    :short => "-p pass",
    :large => "--password pass",
    :format => String,
    :description => "MySQL password. Leave unset to be prompted for it",
    :proc => lambda { |o, options|
        options[:backend] = :mysql
        options[:passwd]  = o
    }
}

DBNAME={
    :name => "dbname",
    :short => "-d dbname",
    :large => "--dbname dbname",
    :format => String,
    :description => "MySQL DB name for OpenNebula",
    :proc => lambda { |o, options|
        options[:backend] = :mysql
        options[:db_name] = o
    }
}

###############################################################################
# Slave MySQL options
###############################################################################
SLAVE_SERVER={
    :name => "slave-server",
    :large => "--slave-server host",
    :format => String,
    :description => "Slave MySQL server hostname or IP. Defaults to localhost",
    :proc => lambda { |o, options|
        options[:slave_backend] = :mysql
        options[:slave_server]  = o
    }
}

SLAVE_PORT={
    :name => "slave-port",
    :large => "--slave-port port",
    :format => String,
    :description => "Slave MySQL server port. Defaults to 3306",
    :proc => lambda { |o, options|
        options[:slave_backend] = :mysql
        options[:slave_port]  = o
    }
}

SLAVE_USERNAME={
    :name => "slave-username",
    :large => "--slave-username user",
    :format => String,
    :description => "Slave MySQL username",
    :proc => lambda { |o, options|
        options[:slave_backend] = :mysql
        options[:slave_user]    = o
    }
}

SLAVE_PASSWORD={
    :name => "slave-password",
    :large => "--slave-password pass",
    :format => String,
    :description => "Slave MySQL password. Leave unset to be prompted for it",
    :proc => lambda { |o, options|
        options[:slave_backend] = :mysql
        options[:slave_passwd]  = o
    }
}

SLAVE_DBNAME={
    :name => "slave-dbname",
    :large => "--slave-dbname dbname",
    :format => String,
    :description => "Slave MySQL DB name for OpenNebula",
    :proc => lambda { |o, options|
        options[:slave_backend] = :mysql
        options[:slave_db_name] = o
    }
}

SLAVE_BACKUP={
    :name => "slave-backup",
    :large => "--slave-backup file",
    :description => "Use this file to store SQL dump",
    :format => String
}

###############################################################################
# Extra options
###############################################################################

EXTRA={
    :name => "extra",
    :large => "--extra arg",
    :description => "Extra args",
    :format => Array
}

###############################################################################
# Live operation options
###############################################################################

START_TIME = {
    :name   => "start_time",
    :short  => "-s TIME",
    :large  => "--start TIME" ,
    :description => "First time to process",
    :format => Time
}

END_TIME = {
    :name   => "end_time",
    :short  => "-e TIME",
    :large  => "--end TIME" ,
    :description => "Last time to process",
    :format => Time
}

ID = {
    :name   => "id",
    :short  => "-i ID",
    :large  => "--id ID" ,
    :description => "Filter by ID",
    :format => Numeric
}

XPATH = {
    :name   => "xpath",
    :short  => "-x ID",
    :large  => "--xpath ID" ,
    :description => "Filter by xpath",
    :format => String
}

EXPR= {
    :name   => "expr",
    :short  => "-e ID",
    :large  => "--expr ID" ,
    :description => "Filter by expression (UNAME=oneadmin)",
    :format => String
}

DRY= {
    :name   => "dry",
    :large  => "--dry" ,
    :description => "Do not write in the database, output xml"
}

DELETE= {
    :name   => "delete",
    :short  => "-d",
    :large  => "--delete" ,
    :description => "Delete all matched xpaths"
}

cmd=CommandParser::CmdParser.new(ARGV) do
    description <<-EOT.unindent
        This command enables the user to manage the OpenNebula database. It
        provides information about the DB version, means to upgrade it to the
        latest version, and backup tools.
    EOT

    ###########################################################################
    # Global options
    ###########################################################################
    set :option, CommandParser::OPTIONS
    set :option, [SQLITE, SERVER, PORT, USERNAME, PASSWORD, DBNAME]

    ###########################################################################
    # Backup
    ###########################################################################
    backup_desc = <<-EOT.unindent
        Dumps the DB to a file specified in the argument
    EOT

    command :backup, backup_desc, [:output_file, nil],
            :options=>[FORCE, FEDERATED] do

        begin
            helper = OneDB.new(options)
            helper.backup(args[0], options)
        rescue Exception => e
            [-1, e.message]
        end
    end

    ###########################################################################
    # Version
    ###########################################################################
    version_desc = <<-EOT.unindent
        Prints the current DB version.
        Use -v flag to see also OpenNebula version
    EOT

    command :version , version_desc do
        begin
            helper = OneDB.new(options)
            helper.version(options)
        rescue Exception => e
            [-1, e.message]
        end
    end

    ###########################################################################
    # History
    ###########################################################################
    history_desc = <<-EOT.unindent
        Prints the upgrades history
    EOT

    command :history , history_desc do
        begin
            helper = OneDB.new(options)
            helper.history
        rescue Exception => e
            [-1, e.message]
        end
    end

    ###########################################################################
    # Restore
    ###########################################################################
    restore_desc = <<-EOT.unindent
        Restores the DB from a backup file. Only restores backups generated
        from the same backend (SQLite or MySQL)
    EOT

    command :restore , restore_desc, :backup_file,
            :options=>[FORCE, FEDERATED] do

        begin
            helper = OneDB.new(options)
            helper.restore(args[0], options)
        rescue Exception => e
            [-1, e.message]
        end
    end

    ###########################################################################
    # Upgrade
    ###########################################################################
    upgrade_desc = <<-EOT.unindent
        Upgrades the DB to the latest version
        where <version> : DB version (e.g. 1, 3) to upgrade.
                          By default the DB is upgraded to the latest version
    EOT

    command :upgrade , upgrade_desc, [:version, nil], :options=>[FORCE,BACKUP] do
        begin
            helper = OneDB.new(options)
            helper.upgrade(args[0], options)
        rescue Exception => e
            [-1, e.message]
        end
    end

    ###########################################################################
    # fsck
    ###########################################################################
    fsck_desc = <<-EOT.unindent
        Checks the consistency of the DB, and fixes the problems found
    EOT

    command :fsck, fsck_desc, :options=>[FORCE,BACKUP] do
        begin
            helper = OneDB.new(options)
            helper.fsck(options)
        rescue Exception => e
            [-1, e.message]
        end
    end

    ###########################################################################
    # Migrate vcenter 54
    ###########################################################################
    vcenter_one54_desc = <<-EOT.unindent
        Migrate VM and templates so they can be used by OpenNebula 5.4
    EOT

    command :"vcenter-one54", vcenter_one54_desc, :options=>[FORCE,BACKUP] do

        begin
            helper = OneDB.new(options)
            helper.vcenter_one54(options)
        rescue Exception => e
            [-1, e.message]
        end
    end

    ###########################################################################
    # Patch
    ###########################################################################
    patch_desc = <<-EOT.unindent
        Applies a database patch file
    EOT

    command :patch , patch_desc, :file, :options=>[BACKUP, EXTRA] do
        begin
            helper = OneDB.new(options)
            helper.patch(args[0], options)
        rescue Exception => e
            [-1, e.message]
        end
    end

    ###########################################################################
    # Migrate SQLite to MySQL
    ###########################################################################
    sqlite2mysql_desc = <<-EOT.unindent
        Migrates a SQLite OpenNebula Database to MySQL
    EOT

    command :sqlite2mysql , sqlite2mysql_desc, :options=>[BACKUP] do
        begin
            options[:backend] = :sqlite
            sqlite = OneDB.new(options)

            options[:backend] = :mysql
            mysql = OneDB.new(options)

            mysql.sqlite2mysql(options, sqlite)
        rescue Exception => e
            [-1, e.message]
        end
    end

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

    ###########################################################################
    # Purge history
    ###########################################################################
    purge_history_desc = <<-EOT.unindent
        Deletes all but the last history records from non DONE VMs

        #{LIVE_ACTION_HELP}
    EOT

    command :'purge-history', purge_history_desc,
            :options => [START_TIME, END_TIME] do
        begin
            action = OneDBLive.new
            action.purge_history(options)
        rescue Exception => e
            puts e.message
            pp e.backtrace
            [-1, e.message]
        end

        0
    end

    ###########################################################################
    # Purge VMs in DONE state
    ###########################################################################
    purge_done_desc = <<-EOT.unindent
        Deletes all VMs in DONE state

        #{LIVE_ACTION_HELP}
    EOT

    command :'purge-done', purge_done_desc,
            :options => [START_TIME, END_TIME] do
        begin
            action = OneDBLive.new
            action.purge_done_vm(options)
        rescue Exception => e
            puts e.name
            pp e.backtrace
            [-1, e.message]
        end

        0 # exit code
    end

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

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

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

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

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

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

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

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

        Delete cache attribute in all disks in poweroff:

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

        #{LIVE_ACTION_HELP}
    EOT

    command :'change-body', change_body_desc, :object, :xpath, [:value, nil],
            :options => [ID, XPATH, EXPR, DRY, DELETE] do
        begin
            action = OneDBLive.new
            action.change_body(args[0], args[1], args[2], options)
        rescue Exception => e
            puts e.message
            pp e.backtrace
            [-1, e.message]
        end

        0 # exit code
    end
end
