#!/usr/bin/env ruby

# -------------------------------------------------------------------------- #
# 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.                                             #
#--------------------------------------------------------------------------- #

ONE_LOCATION = ENV['ONE_LOCATION']

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

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

$LOAD_PATH << RUBY_LIB_LOCATION
$LOAD_PATH << ONEFLOW_LOCATION
$LOAD_PATH << RUBY_LIB_LOCATION + '/cli'
$LOAD_PATH << RUBY_LIB_LOCATION + '/oneflow/lib'

require 'json'
require 'English'
require 'tempfile'

require 'command_parser'
require 'opennebula/oneflow_client'
require 'cli_helper'
require 'one_helper/oneflowtemplate_helper'
require 'one_helper/onetemplate_helper'

USER_AGENT = 'CLI'

# Base Path representing the resource to be used in the requests
RESOURCE_PATH = '/service_template'

RESOURCE_DOCUMENT_TYPE = '101'

def check_document_type(response)
    if CloudClient.is_error?(response)
        return if response.code == '500' # could be wrong document_type; skip this id

        exit_with_code response.code.to_i, response.to_s
    else
        document_type = JSON.parse(response.body)['DOCUMENT']['TYPE']
        if document_type == RESOURCE_DOCUMENT_TYPE
            yield
        end
    end
end

CommandParser::CmdParser.new(ARGV) do
    MULTIPLE = {
        :name  => 'multiple',
        :short => '-m x',
        :large => '--multiple x',
        :format => Integer,
        :description => 'Instance multiple templates'
    }

    RECURSIVE = {
        :name  => 'recursive',
        :short => '-R',
        :large => '--recursive',
        :description => 'Clone the template recursively (templates and images)'
    }

    RECURSIVE_TEMPLATES = {
        :name  => 'recursive_templates',
        :large => '--recursive-templates',
        :description => 'Clone the template recursively (just templates)'
    }

    DELETE_TEMPLATES = {
        :name => 'delete_templates',
        :large => '--delete-vm-templates',
        :description => 'Delete associated VM templates when deleting ' \
                        'service template'
    }

    DELETE_IMAGES = {
        :name => 'delete_images',
        :large => '--delete-images',
        :description => 'Delete associated VM templates and images when ' \
                        'deleting service template'
    }

    FORMAT = [OpenNebulaHelper::JSON, OpenNebulaHelper::YAML]

    usage '`oneflow-template` <command> [<args>] [<options>]'
    version OpenNebulaHelper::ONE_VERSION

    set :option, Service::DEFAULT_OPTIONS
    set :option, CommandParser::VERSION
    set :option, CommandParser::HELP

    # create helper object
    helper = OneFlowTemplateHelper.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,
        Service.rname_to_id_desc('SERVICE TEMPLATE') do |arg|
        Service.rname_to_id(arg, 'SERVICE TEMPLATE')
    end

    set :format, :templateid_list,
        Service.list_to_id_desc('SERVICE TEMPLATE') do |arg|
        Service.list_to_id(arg, 'SERVICE TEMPLATE')
    end

    ###

    list_desc = <<-EOT.unindent
        List the available Service Templates
    EOT

    command :list, list_desc,
            :options => FORMAT + CLIHelper::OPTIONS + [OpenNebulaHelper::DESCRIBE] do
        helper.list_service_template_pool(helper.client(options), options)
    end

    ###

    top_desc = <<-EOT.unindent
        List the available Service Templates continuously
    EOT

    command :top,
            top_desc,
            :options => [Service::JSON_FORMAT,
                         Service::TOP,
                         CLIHelper::DELAY] do
        Signal.trap('INT') { exit(-1) }

        helper.top_service_template_pool(helper.client(options), options)

        0
    end

    ###

    show_desc = <<-EOT.unindent
        Show detailed information of a given Service Template
    EOT

    command :show, show_desc, :templateid, :options => FORMAT do
        helper.format_resource(helper.client(options), args[0], options)
    end

    ###

    create_desc = <<-EOT.unindent
        Create a new Service Template from a json service definition

        #{OpenNebulaHelper::TEMPLATE_INPUT}
    EOT

    command :create, create_desc, [:file, nil], :options => Service::JSON_FORMAT do
        client = helper.client(options)

        if args[0]
            template = File.read(args[0])
        elsif !(stdin = OpenNebulaHelper.read_stdin).empty?
            template = stdin
        end

        if !template
            STDERR.puts 'A template must be provided'
            exit(-1)
        end

        response = client.post(RESOURCE_PATH, template)

        if CloudClient.is_error?(response)
            [response.code.to_i, response.to_s]
        else
            if options[:json]
                [0, response.body]
            else
                puts "ID: #{JSON.parse(response.body)['DOCUMENT']['ID']}"

                0
            end
        end
    end

    ###

    delete_desc = <<-EOT.unindent
        Delete a given Service Template
    EOT

    command :delete,
            delete_desc,
            [:range, :templateid_list],
            :options => [DELETE_TEMPLATES, DELETE_IMAGES] do
        client = helper.client(options)

        # :templates => delete just VM templates
        # :all       => delete VM templates and images
        # :none      => do not delete anything
        if options.key?(:delete_templates)
            delete = 'templates'
        elsif options.key?(:delete_images)
            delete = 'all'
        else
            delete = 'none'
        end

        body                = {}
        body['delete_type'] = delete

        Service.perform_actions(args[0]) do |template_id|
            response = client.get("#{RESOURCE_PATH}/#{template_id}")
            check_document_type response do
                client.delete("#{RESOURCE_PATH}/#{template_id}", body.to_json)
            end
        end
    end

    ###

    instantiate_desc = <<-EOT.unindent
        Instantiate a Service Template
        Optionally append modifications with a json service definition

        #{OpenNebulaHelper::TEMPLATE_INPUT}
    EOT

    command :instantiate, instantiate_desc, :templateid, [:file, nil],
            :options => [MULTIPLE, Service::JSON_FORMAT, Service::TOP] do
        number = options[:multiple] || 1
        params = {}
        rc     = 0
        client = helper.client(options)

        if args[1]
            template = File.read(args[1])
        elsif !(stdin = OpenNebulaHelper.read_stdin).empty?
            template = stdin
        end
        params['merge_template'] = JSON.parse(template) if template

        unless params['merge_template']
            response = client.get("#{RESOURCE_PATH}/#{args[0]}")

            if CloudClient.is_error?(response)
                rc = [response.code.to_i, response.to_s]
                break
            end

            params['merge_template'] = {}
            body = JSON.parse(response.body)['DOCUMENT']['TEMPLATE']['BODY']

            # Check global user inputs
            user_inputs = helper.user_inputs(body['user_inputs'])
            params['merge_template'].merge!(user_inputs) unless user_inputs.nil?

            # Check role level user inputs
            user_inputs_attrs = helper.role_user_inputs(body['roles'])
            params['merge_template'].merge!(user_inputs_attrs) unless user_inputs_attrs.nil?

            # Check vnets attributes
            vnets = helper.networks(body['networks'])
            params['merge_template'].merge!(vnets) unless vnets.nil?
        end

        json = Service.build_json_action('instantiate', params)

        number.times do
            response = client.post("#{RESOURCE_PATH}/#{args[0]}/action", json)

            if CloudClient.is_error?(response)
                rc = [response.code.to_i, response.to_s]
                break
            else
                if options[:json]
                    rc = [0, response.body]
                    break
                else
                    rc = 0
                    puts "ID: #{JSON.parse(response.body)['DOCUMENT']['ID']}"
                end
            end
        end

        rc
    end

    ###

    chgrp_desc = <<-EOT.unindent
        Changes the service template group
    EOT

    command :chgrp, chgrp_desc, [:range, :templateid_list], :groupid do
        client = helper.client(options)

        Service.perform_actions(args[0]) do |service_id|
            params = {}
            params['group_id'] = args[1].to_i

            json = Service.build_json_action('chgrp', params)

            client.post("#{RESOURCE_PATH}/#{service_id}/action", json)
        end
    end

    ###

    chown_desc = <<-EOT.unindent
        Changes the service template owner and group
    EOT

    command :chown,
            chown_desc,
            [:range, :templateid_list],
            :userid,
            [:groupid, nil] do
        client = helper.client(options)

        Service.perform_actions(args[0]) do |service_id|
            params = {}
            params['owner_id'] = args[1]
            params['group_id'] = args[2] if args[2]

            json = Service.build_json_action('chown', params)

            client.post("#{RESOURCE_PATH}/#{service_id}/action", json)
        end
    end

    ###

    chmod_desc = <<-EOT.unindent
        Changes the service template permissions
    EOT

    command :chmod, chmod_desc, [:range, :templateid_list], :octet do
        if !/\A\d+\z/.match(args[1])
            STDERR.puts "Invalid '#{args[1]}' octed permissions"
            exit(-1)
        end

        client = helper.client(options)

        Service.perform_actions(args[0]) do |service_id|
            params = {}
            params['octet'] = OpenNebulaHelper.to_octet(args[1])

            json = Service.build_json_action('chmod', params)

            client.post("#{RESOURCE_PATH}/#{service_id}/action", json)
        end
    end

    ###

    clone_desc = <<-EOT.unindent
        Creates a new Service Template from an existing one
    EOT

    command :clone,
            clone_desc,
            :templateid,
            :name,
            :options => [RECURSIVE, RECURSIVE_TEMPLATES] do
        params = {}
        params['name'] = args[1]

        if options.key?(:recursive)
            params['recursive'] = 'all'
        elsif options.key?(:recursive_templates)
            params['recurisve'] = 'templates'
        else
            params['recursive'] = 'none'
        end

        json   = Service.build_json_action('clone', params)
        client = helper.client(options)

        response = client.post("#{RESOURCE_PATH}/#{args[0]}/action", json)

        if CloudClient.is_error?(response)
            [response.code.to_i, response.to_s]
        else
            if options[:json]
                [0, response.body]
            else
                puts "ID: #{JSON.parse(response.body)['DOCUMENT']['ID']}"

                0
            end
        end
    end

    ###

    rename_desc = <<-EOT.unindent
        Renames the Service Template
    EOT

    command :rename, rename_desc, :templateid, :name do
        Service.perform_action(args[0]) do |template_id|
            params = {}
            params['name'] = args[1]

            json   = Service.build_json_action('rename', params)
            client = helper.client(options)

            client.post("#{RESOURCE_PATH}/#{template_id}/action", json)
        end
    end

    ###

    update_desc = <<-EOT.unindent
        Update the template contents. If a path is not provided the editor will
        be launched to modify the current content.
    EOT

    command :update, update_desc, :templateid, [:file, nil] do
        template_id = args[0]
        client      = helper.client(options)

        if args[1]
            path = args[1]
        else
            response = client.get("#{RESOURCE_PATH}/#{template_id}")

            if CloudClient.is_error?(response)
                exit_with_code response.code.to_i, response.to_s
            else
                document = JSON.parse(response.body)['DOCUMENT']
                template = document['TEMPLATE']['BODY']

                tmp  = Tempfile.new(template_id.to_s)
                path = tmp.path

                tmp.write(JSON.pretty_generate(template))
                tmp.flush

                if ENV['EDITOR']
                    editor_path = ENV['EDITOR']
                else
                    editor_path = OpenNebulaHelper::EDITOR_PATH
                end

                system("#{editor_path} #{path}")

                unless $CHILD_STATUS.exitstatus.zero?
                    STDERR.puts 'Editor not defined'
                    exit(-1)
                end

                tmp.close
            end
        end

        response = client.put("#{RESOURCE_PATH}/#{template_id}",
                              File.read(path))

        if CloudClient.is_error?(response)
            [response.code.to_i, response.to_s]
        else
            0
        end
    end
end
