#!/usr/bin/env ruby
#
# frozen_string_literal: true

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

# clone: clones the image from the datastore (non-persistent images)
# ARGUMENTS: fe:SOURCE host:remote_system_ds/disk.i vmid dsid
#   - fe is the front-end hostname
#   - SOURCE is the path of the disk image in the form DS_BASE_PATH/disk
#   - host is the target host to deploy the VM
#   - remote_system_ds is the path for the system datastore in the host
#   - vmid is the id of the VM
#   - dsid is the target datastore (0 is the system datastore)

ONE_LOCATION = ENV['ONE_LOCATION'] unless defined?(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

# %%RUBYGEMS_SETUP_BEGIN%%
require 'load_opennebula_paths'
# %%RUBYGEMS_SETUP_END%%

$LOAD_PATH << RUBY_LIB_LOCATION

require_relative '../lib/datastore'
require_relative '../lib/tm_action'
require_relative '../lib/tm_cache'
require 'json'

arg_src   = ARGV[0]
arg_dst   = ARGV[1]
arg_vmid  = ARGV[2]
_arg_dsid = ARGV[3]

clone = TransferManager::Action.new(:vm_id => arg_vmid, :action_name => 'clone')

src = TransferManager::Action::Location.new(arg_src)
dst = TransferManager::Action::Location.new(arg_dst)

# --------------------------------------------------------------------------
# Image & Datastore information
# --------------------------------------------------------------------------

disk_id = dst.base.to_s.split('.')[-1]
img_id = clone.disk_attribute(disk_id, 'IMAGE_ID').to_i
image = OpenNebula::Image.new_with_id(img_id, clone.one)

rc_img = image.info

raise rc_img.message.to_s if OpenNebula.is_error?(rc_img)

ds = TransferManager::Datastore.from_image_ds(:image  => image,
                                              :client => clone.one)

# --------------------------------------------------------------------------
# Cache management
# --------------------------------------------------------------------------

if ds['/DATASTORE/TEMPLATE/CACHE_ENABLE'].to_s.upcase == 'YES' && image['/IMAGE/PERSISTENT'] == '0'
    cache_util = '/var/tmp/one/tm/lib/tm_cache.rb'

    config = {
        :ds_xml    => ds.xml_to_hash,
        :hostname  => dst.host,
        :one_fe    => src.host,
        :upstreams => (
            ds['/DATASTORE/TEMPLATE/CACHE_UPSTREAMS'] || ''
        ).split(',').reject(&:empty?)
    }

    image_id = File.basename(src.path)
    modtime = image['/IMAGE/MODTIME']
    size = File.size(src.path)
    fe_default = src.path

    cache_script = "ruby #{cache_util} 'get' " \
                   "'#{JSON.dump(config)}' " \
                   "'#{image_id}' " \
                   "'#{modtime}' " \
                   "'#{size}' " \
                   "'#{fe_default}'"

    OpenNebula::DriverLogger.log_debug("Initiating cache lookup for image #{img_id}")

    rc_cache = clone.ssh(:cmds => cache_script,
                         :host => dst.host,
                         :forward => true,
                         :nostdout => false,
                         :nostderr => false,
                         :err_msg => 'Cache lookup failed')

    exit rc_cache.code unless rc_cache.code.zero?

    if (data = rc_cache.stdout.empty? ? nil : JSON.parse(rc_cache.stdout))
        src.path, src.host = data.values_at('src', 'host')
        OpenNebula::DriverLogger.log_debug("Using cached image found on #{src.host} at #{src.path}")
    end

end

# --------------------------------------------------------------------------
# Cloning DISK
# --------------------------------------------------------------------------
clone.make_dst_path(dst, false)
clone.enable_local_monitoring(dst, 'ssh')

OpenNebula::DriverLogger.log_info "Cloning #{src.path} to #{dst.path}"

disk_format = clone.disk_format(disk_id)
disk_sparse = clone.sparse?(disk_id)

tar_opt = if disk_sparse
              'Sf'
          else
              'f'
          end

script, host =
    case disk_format
    when :raw
        # File layout at destination:
        #
        # └── 7
        #    ├── deployment.0
        #    ├── disk.0
        #    ├── disk.1
        #    ├── ds.xml
        #    └── vm.xml
        [<<~SCRIPT, src.host]
            set -e -o pipefail

            if [ -d "#{src.path}.snap" ]; then
                SRC_SNAP="#{src.base}.snap"
            fi

            tar -C #{src.dir} --transform='flags=r;s|#{src.base}|#{dst.base}|' -c#{tar_opt} - #{src.base} \$SRC_SNAP | \
                ssh #{dst.host} "tar -x#{tar_opt} - -C #{dst.dir}"
        SCRIPT
    when :qcow2
        # File layout at destination:
        #
        # └── 7
        #    ├── deployment.0
        #    ├── disk.0 -> disk.0.snap/0
        #    ├── disk.0.snap
        #    │   ├── 0
        #    ├── disk.1
        #    ├── ds.xml
        #    └── vm.xml
        #
        # Notes of snapshots structure:
        #  - If exists, rebuild the symlink to point <hash>.snap/<snapid>,
        #    instead of disk.<disk_id>.snap/<snapid>
        #  - If it does not exist, create it.
        [<<~SCRIPT, dst.host]
            set -e -o pipefail

            pushd #{dst.dir}
            rm -rf #{dst.path} #{dst.path}.snap

            ssh -n #{src.host} "
                if [ -d '#{src.path}.snap' ]; then
                    SRC_SNAP='#{src.base}.snap'
                fi

                tar -C #{src.dir} --transform='flags=r;s|#{src.base}|#{dst.base}|' \
                    -c#{tar_opt} - #{src.base} \\\$SRC_SNAP" \
            | tar -x#{tar_opt} - -C #{dst.dir}

            if [ -L '#{dst.path}' ] && [ -d '#{dst.path}.snap' ]; then
                PREVIOUS_SNAP="$(readlink "#{dst.path}" | xargs basename)"
                ln -sf disk.#{disk_id}.snap/\$PREVIOUS_SNAP disk.#{disk_id}
            elif [ -f '#{dst.path}' ] && [ ! -e '#{dst.path}.snap' ]; then
                mkdir '#{dst.path}.snap'
                mv '#{dst.path}' '#{dst.path}.snap/0'
                ln -s '#{dst.base}.snap/0' '#{dst.path}'
            else
                echo 'Invalid qcow2 directory structure' >&2
                exit 1
            fi

            popd
        SCRIPT
    end

rc = clone.ssh(:cmds => script,
               :host => host,
               :forward => true,
               :nostdout => false,
               :nostderr => false,
               :err_msg => "copying #{src} to #{dst} from #{host} (format: #{disk_format})")

exit(rc.code) if rc.code != 0

# --------------------------------------------------------------------------
# Resize disk if needed
# --------------------------------------------------------------------------
(size, original_size) = clone.disk_sizes(disk_id).values_at(:current, :original)

if original_size && size > original_size
    rc = clone.resize_disk(dst, size, disk_sparse)

    exit(rc.code) if rc.code != 0
end
