#!/usr/bin/env ruby

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

require 'net/http'
require 'uri'
require 'json'
require 'base64'
require 'rexml/document'
require 'time'
require 'digest/md5'

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

    #---------------------------------------------------------------------------
    # Default Configuration parameters for the Driver
    #---------------------------------------------------------------------------
    DEFAULTS = {
        :url => 'https://images.linuxcontainers.org',
        :sizemb => 1024,
        :fs => 'ext4',
        :format => 'raw',
        :agent => 'OpenNebula',
        :tested_apps => %w[alpine centos debian ubuntu fedora devuan],
        :skip_untested => 'yes'
    }

    # TODO: Make configurable
    TEMPLATE = "
HYPERVISOR = \"lxd\"
CPU = \"1\"
MEMORY = \"768\"
LXD_SECURITY_PRIVILEGED = \"yes\"
GRAPHICS = [
    LISTEN  =\"0.0.0.0\",
    TYPE  =\"vnc\"
]
CONTEXT = [
    NETWORK  =\"YES\",
    SSH_PUBLIC_KEY  =\"$USER[SSH_PUBLIC_KEY]\",
    SET_HOSTNAME  =\"$NAME\"
]"

    #---------------------------------------------------------------------------
    # Configuration varibales
    #   :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
    #---------------------------------------------------------------------------
    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)
        req['User-Agent'] = @options[:agent]

        response = @http_client.request(req)

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

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

    # Get the list of appliances
    def fetch_appliances
        first_level = '/images/'

        open_connection

        rc, body = get(first_level)

        return rc, body if rc != 0

        distros = body.scan(%r{a href="([a-z].*/)">})
        distros.map! {|d| d.join.chomp('/') }

        if @options[:skip_untested] == 'yes'
            distros.delete_if {|d| !@options[:tested_apps].include?(d) }
        end

        tree = {}

        distros.each do |distro|
            d_suf = "#{distro}/"
            rc, body = get(first_level + d_suf)

            next if rc != 0

            versions = body.scan(%r{a href="(.*/)">})
            versions.shift # Remove first entry ("Parent Directory")

            version_path = {}
            versions.each do |version|
                path = "#{first_level}#{d_suf}#{version[0]}amd64/default/"
                rc, body = get(path)

                next if rc != 0

                release_dates = body.scan(%r{a href="(.*/)">})

                # Previous release_dates array leaves a point in the html page
                release_date  = release_dates.last[0]
                version_path[version[0]] = "#{path}#{release_date}rootfs.tar.xz"
            end

            tree[d_suf] = version_path
        end

        appstr = ''

        #-----------------------------------------------------------------------
        # Generate the container app information
        #-----------------------------------------------------------------------
        tree.each do |distro, value|
            value.each do |version, path|
                description = "Downloaded from #{@options[:url]}"
                regtime = app_time(path).to_s

                data = {
                    'NAME' => "#{distro[0...-1]}_#{version[0...-1]} - LXD",
                    'SIZE' => @options[:sizemb],
                    'PUBLISHER' => @options[:url],
                    'FORMAT' => @options[:format],
                    'REGTIME' => regtime,
                    'SOURCE' => app_url(path).to_s,
                    'MD5' => md5(regtime),
                    'IMPORT_ID' => '-1',
                    'ORIGIN_ID' => '-1',
                    'VERSION' => '1.0',
                    'TYPE' => 'IMAGE',
                    'DESCRIPTION' => description,
                    'TAGS' => ''
                }

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

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

                data = { 'APPTEMPLATE64' => tmpl64, 'VMTEMPLATE64' => TEMPLATE }
                data.each do |key, val|
                    print_var(tmpl, key, Base64.strict_encode64(val))
                end

                appstr << "APP=\"#{Base64.strict_encode64(tmpl)}\"\n"
            end
        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

    # Returns build date based on image path
    def app_time(path)
        m1 = 'amd64/default/./'
        m2 = '/rootfs.tar.xz'

        buildate = path[/#{m1}(.*?)#{m2}/m, 1]
        buildate = DateTime.strptime(buildate, '%Y%m%d_%H:%M')
        buildate.to_time.to_i
    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. Outpust 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" }

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

    puts LinuxContainersMarket.new(options).fetch_appliances
rescue StandardError
    nil
end
