#!/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'] 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 'net/http'
require 'uri'
require 'json'
require 'base64'
require 'rexml/document'
require 'time'
require 'digest/md5'

require_relative 'lxd'

#-------------------------------------------------------------------------------
#
#-------------------------------------------------------------------------------
class LinuxContainersMarket

    #---------------------------------------------------------------------------
    # Default Configuration parameters for the Driver
    #   :url of linuxcontainers market place
    #   :sizemb default size for container images
    #   :fs filesystem for the image file
    #   :format for the image file, qcow2, raw
    #   :agent for HTTP client
    #   :memory Guest RAM
    #   :cpu Guest cpu share with host
    #   :vcpu Guest cores
    #   :privileged container idmap
    #---------------------------------------------------------------------------
    DEFAULTS = {
        :url           => 'https://images.linuxcontainers.org/',
        :sizemb        => 2048,
        :fs            => 'ext4',
        :format        => 'raw',
        :agent         => 'OpenNebula',
        :tested_apps   => ['almalinux', 'alpine', 'amazonlinux', 'debian', 'devuan',
                           'fedora', 'opensuse', 'oracle', 'rockylinux', 'ubuntu'],
        :skip_untested => 'yes',
        :memory        => '768',
        :cpu           => 1,
        :vcpu          => 2,
        :privileged    => true,
        :raw           => 'lxc.apparmor.profile=unconfined'
    }

    def initialize(options = {})
        @options = DEFAULTS
        @options.merge!(options)

        version_path = File.dirname(__FILE__) + '/../../VERSION'
        @options[:agent] = "OpenNebula #{File.read(version_path)}" \
          if File.exist? version_path

        @http_client = nil
    end

    # Get container information
    def get(path)
        uri = URI(@options[:url] + path)
        req = Net::HTTP::Get.new(uri.request_uri, { 'User-Agent' => @options[:agent] })

        response = @http_client.request(req)

        return 0, response.body if response.is_a? Net::HTTPSuccess

        [response.code.to_i, response.msg]
    end

    # Relevant image.json format fields are
    #
    # {...,
    #   "products": {
    #     $distro:$version:$arch:$variant: {
    #       ...
    #       "versions": {
    #         $release_date: {
    #           "items": {
    #             $container_type: {
    #               ...
    #               "size": int
    #               "path": $path
    #               "sha256": $sha256
    #             }
    #           }
    #         }
    #       }
    #     }
    #   }
    # }

    def fetch_appliances
        image_list_address = '/streams/v1/images.json'

        open_connection
        rc, body = get(image_list_address)

        return rc, body if rc != 0

        image_list = JSON.parse(body)
        appstr = ''

        image_list['products'].each do |img, img_data|
            distro, version, arch, variant = img.split(':')

            next if variant != 'default' || (arch != 'amd64' && arch != 'arm64')
            next if @options[:skip_untested] == 'yes' && !@options[:tested_apps].include?(distro)

            case arch
            when 'arm64'
                arch = 'aarch64'
                name = "#{distro}_#{version} (#{arch})"
            when 'amd64'
                arch = 'x86_64'
                name = "#{distro}_#{version}"
            end

            @options[:arch] = arch

            # Use the last version for each container
            regtime = img_data['versions'].to_a.last[0]

            begin
                path = img_data['versions'][regtime]['items']['root.tar.xz']['path']
            rescue StandardError
                next
            end

            builddate = DateTime.strptime(regtime, '%Y%m%d_%H:%M').to_time.to_i

            data = {
                'NAME'        => name,
                'SIZE'        => @options[:sizemb],
                'PUBLISHER'   => @options[:url],
                'FORMAT'      => @options[:format],
                'REGTIME'     => builddate,
                'SOURCE'      => app_url(path).to_s,
                'MD5'         => md5(regtime),
                'IMPORT_ID'   => '-1',
                'ORIGIN_ID'   => '-1',
                'VERSION'     => '1.0',
                'TYPE'        => 'IMAGE',
                'DESCRIPTION' =>  "Downloaded from #{@options[:url]}",
                'TAGS'        => '',
                'LINK'        => "#{@options[:url]}#{path}",
                'ARCHITECTURE'=> arch,
                'HYPERVISOR'  => 'lxc'
            }

            tmpl = ''
            data.each {|key, val| print_var(tmpl, key, val) }

            tmpl64 = ''
            print_var(tmpl64, 'DRIVER', 'raw')

            data = { 'APPTEMPLATE64' => tmpl64,
                     'VMTEMPLATE64' => LXDMarket.template(@options) }

            data.each do |key, val|
                print_var(tmpl, key, Base64.strict_encode64(val))
            end

            appstr << "APP=\"#{Base64.strict_encode64(tmpl)}\"\n"
        end

        appstr
    ensure
        @http_client&.finish if @http_client
    end

    private

    # Generate the URL for the appliance path of the container. Example:
    #
    # lxd://https://images.linuxcontainers.org/images/ubuntu/xenial/amd64/default/\
    #    ./20181214_07:42/rootfs.tar.xz?size=5120&filesystem=ext4&format=raw
    def app_url(path)
        "lxd://#{@options[:url]}#{path}?size=#{@options[:sizemb]}" \
        "&filesystem=#{@options[:fs]}&format=#{@options[:format]}"
    end

    # Print variable in an APP template
    def print_var(str, name, val)
        return if val.nil?
        return if val.class == String && val.empty?

        str << "#{name}=\"#{val}\"\n"
    end

    # Returns an md5 from a combination of the @option hash and an input string
    def md5(string)
        Digest::MD5.hexdigest("#{@options} #{string}")
    end

    # Opens a TCP connection to @options[:url]
    def open_connection
        @http_client&.finish if @http_client

        http_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY'] # for ruby 1.9.3

        if http_proxy
            http_proxy = 'http://' + http_proxy if http_proxy !~ /^http/

            p_uri   = URI(http_proxy)
            p_host  = p_uri.host
            p_port  = p_uri.port
        else
            p_host  = nil
            p_port  = nil
        end

        uri = URI(@options[:url])

        response = Net::HTTP.get_response(uri)

        if response.is_a? Net::HTTPRedirection
            @options[:url] = response['location']
            uri = URI(@options[:url])
        end

        @http_client = Net::HTTP.start(uri.hostname, uri.port, p_host, p_port,
                                       :use_ssl => uri.scheme == 'https')
    end

end

################################################################################
# Main Program. Its output is the list of marketplace appliances
################################################################################
def set_option(option, doc, name, path)
    option[name] = doc.elements[path].text if doc.elements[path]
end

begin
    options     = {}
    drv_message = Base64.decode64(ARGV[0])
    doc = REXML::Document.new(drv_message).root

    pre = 'MARKETPLACE/TEMPLATE'

    data = {
        :url           => "#{pre}/ENDPOINT",
        :sizemb        => "#{pre}/IMAGE_SIZE_MB",
        :fs            => "#{pre}/FILESYSTEM",
        :format        => "#{pre}/FORMAT",
        :skip_untested => "#{pre}/SKIP_UNTESTED",
        :memory        => "#{pre}/MEMORY",
        :cpu           => "#{pre}/CPU",
        :vcpu          => "#{pre}/VCPU",
        :privileged    => "#{pre}/PRIVILEGED"
    }

    data.each {|key, value| set_option(options, doc, key, value) }

    str = LinuxContainersMarket.new(options).fetch_appliances
    puts str
rescue StandardError
    STDERR.puts str
    exit(-1)
end
