#!/usr/bin/env ruby

# -------------------------------------------------------------------------- #
# Copyright 2002-2026, 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.                                             #
#--------------------------------------------------------------------------- #

ONE_LOCATION = ENV['ONE_LOCATION']

if !ONE_LOCATION
    LIB_LOCATION      = '/usr/lib/one'
    RUBY_LIB_LOCATION = '/usr/lib/one/ruby'
    GEMS_LOCATION     = '/usr/share/one/gems'
else
    LIB_LOCATION      = ONE_LOCATION + '/lib'
    RUBY_LIB_LOCATION = ONE_LOCATION + '/lib/ruby'
    GEMS_LOCATION     = ONE_LOCATION + '/share/gems'
end

require 'load_opennebula_paths'

$LOAD_PATH << RUBY_LIB_LOCATION
$LOAD_PATH << RUBY_LIB_LOCATION + '/cli'
$LOAD_PATH << LIB_LOCATION      + '/oneprovision/lib'
$LOAD_PATH << LIB_LOCATION      + '/oneflow/lib'

USER_AGENT = 'CLI'

require 'command_parser'
require 'one_helper'
require 'opennebula/oneform_client'

require 'one_helper/oneprovision_helper'

CommandParser::CmdParser.new(ARGV) do
    FORMAT = [OpenNebulaHelper::JSON, OpenNebulaHelper::YAML]

    usage '`oneprovision` <command> [<args>] [<options>]'
    version OpenNebulaHelper::ONE_VERSION

    set :option, OneForm::Client::DEFAULT_OPTIONS
    set :option, CommandParser::VERSION
    set :option, CommandParser::HELP

    FORCE_RECOVER = {
        :name => 'force',
        :short => '-f',
        :large => '--force',
        :description => 'Force recovery of a failed provision',
        :default => false
    }

    FORCE_DELETE = {
        :name => 'force',
        :short => '-f',
        :large => '--force',
        :description => 'Force deletion from any state or with unmanaged resources',
        :default => false
    }

    FROM_DB = {
        :name => 'from_db',
        :large => '--from-db',
        :description => 'Delete only the provision document without deprovisioning',
        :default => false
    }

    WAIT = {
        :name        => 'wait',
        :large       => '--wait',
        :description => 'Follow provision logs after a successful provision creation',
        :default     => false
    }

    ALL_LOGS = {
        :name => 'all',
        :short => '-a',
        :large => '--all',
        :description => 'Show the full provision log history',
        :default => false
    }

    FOLLOW_LOGS = {
        :name => 'follow',
        :short => '-f',
        :large => '--follow',
        :description => 'Poll provision logs continuously',
        :default => false
    }

    HOSTS = {
        :name        => 'hosts',
        :short       => '-h',
        :large       => '--hosts hosts',
        :description => 'IP addresses or hostnames to add as hosts. ' \
                        'Only valid for on-prem providers',
        :format      => Array
    }

    HOST_IDS = {
        :name        => 'host_ids',
        :short       => '-i ids',
        :large       => '--host-ids ids',
        :description => 'OpenNebula host IDs to remove, separated by commas',
        :format      => String
    }

    AR_ID = {
        :name        => 'ar_id',
        :large       => '--ar-id id',
        :description => 'Address Range ID to remove',
        :format      => Integer
    }

    AMOUNT = {
        :name        => 'amount',
        :short       => '-a n',
        :large       => '--amount n',
        :description => 'Number of hosts to add or remove from the provision',
        :format      => Integer
    }

    AMOUNT_IP = {
        :name        => 'amount',
        :short       => '-a n',
        :large       => '--amount n',
        :description => 'Number of public IPs to add or remove from the provision',
        :format      => Integer
    }

    PROVIDER_ID = {
        :name => 'provider_id',
        :short => '-p id',
        :large => '--provider-id id',
        :description => 'Set the provider by ID to create the provision',
        :format => Integer
    }

    DEPLOYMENT_TYPE = {
        :name => 'deployment',
        :short => '-d name',
        :large => '--deployment name',
        :description => 'Specifies the deployment configuration associated with the provision',
        :format => String
    }

    SENSITIVE = {
        :name => 'sensitive',
        :short => '-s',
        :large => '--sensitive',
        :description => 'Include sensitive values information in the output',
        :default => false
    }

    DECODE = {
        :name => 'decode',
        :large => '--decode',
        :description => 'Decode the stored Terraform state',
        :default => false
    }

    WATCH = {
        :name => 'watch',
        :short => '-w',
        :large => '--watch',
        :description => 'Refresh the active lifecycle job continuously',
        :default => false
    }

    helper = OneProvisionHelper.new

    ############################################################################
    # Formatters for arguments
    ############################################################################
    set :format, :groupid, OpenNebulaHelper.rname_to_id_desc('GROUP') do |arg|
        OpenNebulaHelper.rname_to_id(arg, 'GROUP')
    end

    set :format, :userid, OpenNebulaHelper.rname_to_id_desc('USER') do |arg|
        OpenNebulaHelper.rname_to_id(arg, 'USER')
    end

    set :format, :templateid, OneForm::Helpers.rname_to_id_desc('PROVISION') do |arg|
        OneForm::Helpers.rname_to_id(arg, 'PROVISION')
    end

    set :format, :templateid_list, OneForm::Helpers.list_to_id_desc('PROVISION') do |arg|
        OneForm::Helpers.list_to_id(arg, 'PROVISION')
    end

    ########################################################################
    # Commands for interacting with provisions
    ########################################################################

    list_desc = <<-EOT.unindent
        List all provisions.
    EOT

    command :list,
            list_desc,
            :options => (
                FORMAT + CLIHelper::OPTIONS +
                [OpenNebulaHelper::DESCRIBE, SENSITIVE]
            )do
        helper.list(helper.client(options), options)
    rescue Interrupt
        puts
        exit(1)
    end

    top_desc = <<-EOT.unindent
        List the available provisions continuously
    EOT

    command :top,
            top_desc,
            :options => FORMAT + [CLIHelper::DELAY, SENSITIVE] do
        Signal.trap('INT') { exit(1) }
        helper.top(helper.client(options), options)
    rescue Interrupt
        puts
        exit(1)
    end

    show_desc = <<-EOT.unindent
        Show details of a specific provision.
    EOT

    command :show, show_desc, :provision_id, :options => FORMAT + [SENSITIVE] do
        provision_id = args[0].to_i

        helper.show(helper.client(options), provision_id, options)
    rescue Interrupt
        puts
        exit(1)
    end

    create_desc = <<-EOT.unindent
        Create a Provision object based on driver information.
    EOT

    command :create,
            create_desc,
            :driver_name,
            [:file, nil],
            :options => [PROVIDER_ID, DEPLOYMENT_TYPE, WAIT] do
        driver_name = args[0]
        file_path   = args[1]
        provider_id = options[:provider_id]
        deployment  = options[:deployment]

        if provider_id.nil?
            STDERR.puts 'A provider ID is mandatory to create a Provision:'
            STDERR.puts 'Usage: oneprovision create <driver_name> -p <provider_id>'
            exit(-1)
        end

        helper.create(
            helper.client(options), driver_name, file_path,
            options.merge(
                :provider_id => provider_id,
                :deployment  => deployment
            )
        )
    rescue Interrupt
        puts
        exit(1)
    end

    update_desc = <<-EOT.unindent
        Update a provision by ID with the provided patch data.
    EOT

    command :update, update_desc, :provision_id, [:file, nil] do
        provision_id = args[0].to_i
        file_path    = args[1]

        helper.update(helper.client(options), provision_id, file_path)
    rescue Interrupt
        puts
        exit(1)
    end

    rename_desc = <<-EOT.unindent
        Renames the provision
    EOT

    command :rename, rename_desc, :provision_id, :name do
        provision_id = args[0].to_i
        name         = args[1]

        helper.rename(helper.client(options), provision_id, name)
    rescue Interrupt
        puts
        exit(1)
    end

    chgrp_desc = <<-EOT.unindent
        Change the group of a provision.
    EOT

    command :chgrp, chgrp_desc, [:range, :provisionid_list], :groupid do
        provision_ids = args[0]
        group_id      = args[1].to_i

        helper.chgrp(helper.client(options), provision_ids, group_id)
    rescue Interrupt
        puts
        exit(1)
    end

    chown_desc = <<-EOT.unindent
        Change the owner of a provision.
    EOT

    command :chown,
            chown_desc,
            [:range, :provisionid_list],
            :userid,
            [:groupid, nil] do
        provision_ids = args[0]
        user_id       = args[1].to_i
        group_id      = args[2]&.to_i

        helper.chown(helper.client(options), provision_ids, user_id, group_id)
    rescue Interrupt
        puts
        exit(1)
    end

    chmod_desc = <<-EOT.unindent
        Change the permissions of a provision.
    EOT

    command :chmod, chmod_desc, [:range, :provisionid_list], :octet do
        provision_ids = args[0]
        octet         = args[1]

        if !/\A\d+\z/.match(octet)
            STDERR.puts "Invalid '#{octet}' octed permissions"
            exit(-1)
        end

        permissions = OpenNebulaHelper.to_octet(octet)

        helper.chmod(helper.client(options), provision_ids, permissions)
    rescue Interrupt
        puts
        exit(1)
    end

    recover_desc = <<-EOT.unindent
        Recover a provision by retrying its last failed action.
    EOT

    command :recover,
            recover_desc,
            [:range, :provisionid_list],
            :options => [FORCE_RECOVER] do
        provision_ids = args[0]
        force         = options[:force] == true

        helper.recover(helper.client(options), provision_ids, force)
    rescue Interrupt
        puts
        exit(1)
    end

    add_host_desc = <<-EOT.unindent
        Add one or more hosts to a provision by its ID.

        The number of hosts to add is specified using the --amount (or -a) option.
        If the option is not provided, the command defaults to adding a single host.

        Alternatively, hosts can be added by their IP addresses or hostnames using
        the --hosts option. This option is only valid for on-prem providers.

        Examples:
        oneprovision add-host 0
            # Adds 1 host to the provision with ID 0 (default behavior).

        oneprovision add-host 0 --amount 3
            # Adds 3 hosts to the provision with ID 0.

        oneprovision add-host 0 --hosts 1.1.1.1,2.2.2.2
            # Adds hosts with the given IPs to the provision with ID 0.
            # Note: IP/hostname based host addition is only supported for on-prem providers.

        Note: --amount and --hosts are mutually exclusive. Use only one of them at a time.
    EOT

    command :'add-host', add_host_desc, :provision_id, :options => [AMOUNT, HOSTS] do
        provision_id = args[0].to_i
        amount       = options[:amount]
        hosts        = options[:hosts]

        if amount && hosts
            STDERR.puts 'Options --amount and --hosts are mutually exclusive'
            exit(-1)
        end

        helper.add_host(helper.client(options), provision_id, amount, hosts)
    rescue Interrupt
        puts
        exit(1)
    end

    del_host_desc = <<-EOT.unindent
        Remove one or more hosts from a provision by their OpenNebula host IDs.

        Examples:
        oneprovision del-host 5 --host-ids 101
            # Removes the host with OpenNebula ID 101 from provision 5.

        oneprovision del-host 5 --host-ids 101,102,103
            # Removes hosts 101, 102, and 103 from provision 5 in one operation.
    EOT

    command :'del-host', del_host_desc, :provision_id, :options => [HOST_IDS] do
        provision_id = args[0].to_i
        host_ids     = options[:host_ids].to_s.split(',')

        if host_ids.empty? || !host_ids.all? {|id| id.match?(/\A\d+\z/) }
            STDERR.puts 'Option --host-ids is required and must contain valid host IDs'
            exit(-1)
        end

        helper.del_hosts(helper.client(options), provision_id, host_ids.map(&:to_i))
    rescue Interrupt
        puts
        exit(1)
    end

    add_ip_desc = <<-EOT.unindent
        Add a specified number of public IP addresses to a provision.

        Examples:
        oneprovision add-ip 0
            # Allocates 1 public IP address to the provision with ID 0 (default behavior).

        oneprovision add-ip 0 --amount 4
            # Allocates 4 public IP addresses to the provision with ID 0.
    EOT

    command :'add-ip', add_ip_desc, :provision_id, :options => [AMOUNT_IP] do
        provision_id = args[0].to_i
        amount       = options[:amount] || 1

        helper.add_ip(helper.client(options), provision_id, amount)
    rescue Interrupt
        puts
        exit(1)
    end

    del_ip_desc = <<-EOT.unindent
        Removes a previously allocated public IP Address Range from a provision by AR ID.

        Examples:
            oneprovision del-ip 0 --ar-id 100
                # Removes Address Range with ID 100 from the provision with ID 0.
    EOT

    command :'del-ip', del_ip_desc, :provision_id, :options => [AR_ID] do
        provision_id = args[0].to_i
        ar_id        = options[:ar_id]

        unless ar_id
            STDERR.puts 'Option --ar-id is required'
            exit(-1)
        end

        helper.del_ip(helper.client(options), provision_id, ar_id.to_i)
    rescue Interrupt
        puts
        exit(1)
    end

    jobs_desc = <<-EOT.unindent
        Show the active lifecycle job of a provision.
    EOT

    command :job, jobs_desc, :provision_id, :options => FORMAT + [WATCH] do
        provision_id = args[0].to_i

        Signal.trap('INT') { exit(1) } if options[:watch]
        helper.jobs(helper.client(options), provision_id, options)
    rescue Interrupt
        puts
        exit(1)
    end

    tfstate_desc = <<-EOT.unindent
        Show the Terraform state stored for a provision.
    EOT

    command :tfstate, tfstate_desc, :provision_id, :options => FORMAT + [DECODE] do
        provision_id = args[0].to_i

        helper.tfstate(helper.client(options), provision_id, options)
    rescue Interrupt
        puts
        exit(1)
    end

    cancel_desc = <<-EOT.unindent
        Request cancellation of the active lifecycle operation of one or more provisions.
    EOT

    command :cancel, cancel_desc, [:range, :provisionid_list] do
        provision_ids = args[0]

        helper.cancel(helper.client(options), provision_ids)
    rescue Interrupt
        puts
        exit(1)
    end

    delete_desc = <<-EOT.unindent
        Delete one or more provisions and their associated resources.

        Use the --force option to delete a provision from any state or when it
        has unmanaged resources associated.

        Use --from-db only when deprovisioning cannot be completed. It deletes
        the provision document without deleting OpenNebula or provider resources.
    EOT

    command :delete, delete_desc, [:range, :provisionid_list],
            :options => [FORCE_DELETE, FROM_DB] do
        provision_ids = args[0]
        force         = options[:force] == true
        from_db       = options[:from_db] == true

        helper.delete(helper.client(options), provision_ids, force, from_db)
    rescue Interrupt
        puts
        exit(1)
    end

    logs_desc = <<-EOT.unindent
        Show the logs of a provision by ID.
    EOT

    command :logs, logs_desc, :provision_id, :options => [ALL_LOGS, FOLLOW_LOGS] do
        provision_id = args[0].to_i

        helper.logs(helper.client(options), provision_id, options)
    rescue Interrupt
        puts
        exit(1)
    end
end
