From 8b26f099ae09a59beccc23ae894f00f256cba167 Mon Sep 17 00:00:00 2001 From: Alejandro Huertas Herrero Date: Mon, 31 May 2021 13:27:53 +0200 Subject: [PATCH] F #5411: add support for Docker Private Registry (#1248) --- install.sh | 9 +- share/etc/oned.conf | 9 +- src/market_mad/remotes/common/docker.rb | 147 ++++++++++++++++++ src/market_mad/remotes/docker_registry/delete | 29 ++++ src/market_mad/remotes/docker_registry/import | 29 ++++ .../remotes/docker_registry/monitor | 146 +++++++++++++++++ src/market_mad/remotes/dockerhub/monitor | 116 +++----------- 7 files changed, 389 insertions(+), 96 deletions(-) create mode 100644 src/market_mad/remotes/common/docker.rb create mode 100755 src/market_mad/remotes/docker_registry/delete create mode 100755 src/market_mad/remotes/docker_registry/import create mode 100755 src/market_mad/remotes/docker_registry/monitor diff --git a/install.sh b/install.sh index 28143dbd20..1d694bf45c 100755 --- a/install.sh +++ b/install.sh @@ -493,6 +493,7 @@ VAR_DIRS="$VAR_LOCATION/remotes \ $VAR_LOCATION/remotes/market/linuxcontainers \ $VAR_LOCATION/remotes/market/turnkeylinux \ $VAR_LOCATION/remotes/market/dockerhub \ + $VAR_LOCATION/remotes/market/docker_registry \ $VAR_LOCATION/remotes/datastore/iscsi_libvirt \ $VAR_LOCATION/remotes/auth \ $VAR_LOCATION/remotes/auth/plain \ @@ -713,6 +714,7 @@ INSTALL_FILES=( MARKETPLACE_DRIVER_LXC_SCRIPTS:$VAR_LOCATION/remotes/market/linuxcontainers MARKETPLACE_DRIVER_TK_SCRIPTS:$VAR_LOCATION/remotes/market/turnkeylinux MARKETPLACE_DRIVER_DH_SCRIPTS:$VAR_LOCATION/remotes/market/dockerhub + MARKETPLACE_DRIVER_REGISTRY_SCRIPTS:$VAR_LOCATION/remotes/market/docker_registry IPAM_DRIVER_DUMMY_SCRIPTS:$VAR_LOCATION/remotes/ipam/dummy IPAM_DRIVER_PACKET_SCRIPTS:$VAR_LOCATION/remotes/ipam/packet IPAM_DRIVER_VULTR_SCRIPTS:$VAR_LOCATION/remotes/ipam/vultr @@ -2148,7 +2150,8 @@ MARKETPLACE_DRIVER_S3_SCRIPTS="src/market_mad/remotes/s3/import \ src/market_mad/remotes/s3/monitor \ src/market_mad/remotes/s3/S3.rb" -MARKETPLACE_DRIVER_COMMON_SCRIPTS="src/market_mad/remotes/common/lxd.rb" +MARKETPLACE_DRIVER_COMMON_SCRIPTS="src/market_mad/remotes/common/lxd.rb \ + src/market_mad/remotes/common/docker.rb" MARKETPLACE_DRIVER_LXC_SCRIPTS="src/market_mad/remotes/linuxcontainers/import \ src/market_mad/remotes/linuxcontainers/delete \ @@ -2162,6 +2165,10 @@ MARKETPLACE_DRIVER_DH_SCRIPTS="src/market_mad/remotes/dockerhub/import \ src/market_mad/remotes/dockerhub/delete \ src/market_mad/remotes/dockerhub/monitor" +MARKETPLACE_DRIVER_REGISTRY_SCRIPTS="src/market_mad/remotes/docker_registry/import \ + src/market_mad/remotes/docker_registry/delete \ + src/market_mad/remotes/docker_registry/monitor" + #------------------------------------------------------------------------------- # Migration scripts for onedb command, to be installed under $LIB_LOCATION #------------------------------------------------------------------------------- diff --git a/share/etc/oned.conf b/share/etc/oned.conf index d72def9c6b..667120956f 100644 --- a/share/etc/oned.conf +++ b/share/etc/oned.conf @@ -650,7 +650,7 @@ DATASTORE_MAD = [ MARKET_MAD = [ EXECUTABLE = "one_market", - ARGUMENTS = "-t 15 -m http,s3,one,linuxcontainers,turnkeylinux,dockerhub" + ARGUMENTS = "-t 15 -m http,s3,one,linuxcontainers,turnkeylinux,dockerhub,docker_registry" ] #******************************************************************************* @@ -1297,6 +1297,13 @@ MARKET_MAD_CONF = [ PUBLIC = "yes" ] +MARKET_MAD_CONF = [ + NAME = "docker_registry", + SUNSTONE_NAME = "DockerRegistry", + REQUIRED_ATTRS = "BASE_URL", + APP_ACTIONS = "monitor" +] + #******************************************************************************* # Authentication Driver Behavior Definition #******************************************************************************* diff --git a/src/market_mad/remotes/common/docker.rb b/src/market_mad/remotes/common/docker.rb new file mode 100644 index 0000000000..f88af27bac --- /dev/null +++ b/src/market_mad/remotes/common/docker.rb @@ -0,0 +1,147 @@ +#!/usr/bin/ruby + +# -------------------------------------------------------------------------- # +# Copyright 2002-2021, 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 'base64' +require 'digest/md5' +require 'net/http' +require 'uri' + +# Utilities for Docker based marketplaces +module DockerMarket + + class << self + + # TODO: Make configurable + # Returns template to append to all applications + def template + unindent(<<-EOS) + CPU = \"1\" + MEMORY = \"768\" + GRAPHICS = [ + LISTEN =\"0.0.0.0\", + TYPE =\"vnc\" + ] + CONTEXT = [ + NETWORK =\"YES\", + SSH_PUBLIC_KEY =\"$USER[SSH_PUBLIC_KEY]\", + SET_HOSTNAME =\"$NAME\" + ] + OS = [ + KERNEL_CMD=\"console=ttyS0 reboot=k panic=1\" + ]" + EOS + end + + # Makes text unindent + # + # @param str [String] String to use + # + # @return [String] + def unindent(str) + m = str.match(/^(\s*)/) + spaces = m[1].size + str.gsub!(/^ {#{spaces}}/, '') + end + + # Fetch data from path + # + # @param options [Hash] Class options + # @param path [String] URL to fetch + # @param ssl [Boolean] True to use SSL + # + # @return [0/rc, body] + def get(options, path, ssl = true) + # Get proxy params (needed for ruby 1.9.3) + http_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY'] + + p_host = nil + p_port = nil + + if http_proxy + p_uri = URI(http_proxy) + p_host = p_uri.host + p_port = p_uri.port + end + + uri = URI(path) + req = Net::HTTP::Get.new(uri.request_uri) + + req['User-Agent'] = options[:agent] if options[:agent] + + opts = { :use_ssl => true } if ssl + rc = Net::HTTP.start(uri.hostname, + uri.port, + p_host, + p_port, + opts) do |http| + http.request(req) + end + + return [rc.code.to_i, rc.msg] unless rc.is_a? Net::HTTPSuccess + + [0, rc.body] + end + + # Generates app template + # + # @param app [Hash] App information + # + # @returns [String] App information in base64 + def gen_template(app) + tmpl = '' + tmpl64 = '' + + app.each {|key, val| print_var(tmpl, key, val) } + + print_var(tmpl64, 'DRIVER', 'raw') + print_var(tmpl64, 'DEV_PREFIX', 'vd') + + data = { 'APPTEMPLATE64' => tmpl64, 'VMTEMPLATE64' => template } + + data.each do |key, val| + print_var(tmpl, key, Base64.strict_encode64(val)) + end + + "APP=\"#{Base64.strict_encode64(tmpl)}\"\n" + end + + # Prints variable + # + # @param str [String] String to print variable to + # @param name [String] KEY + # @param val [String] Value + # + # @return [String] KEY=VALUE string + 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 str + # + # @param options [Hash] Options to use + # @param string [String] String to combine + def md5(options, string) + Digest::MD5.hexdigest("#{options} #{string}") + end + + end + +end diff --git a/src/market_mad/remotes/docker_registry/delete b/src/market_mad/remotes/docker_registry/delete new file mode 100755 index 0000000000..95ef68deb7 --- /dev/null +++ b/src/market_mad/remotes/docker_registry/delete @@ -0,0 +1,29 @@ +#!/bin/bash + +# -------------------------------------------------------------------------- # +# Copyright 2002-2021, 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. # +# -------------------------------------------------------------------------- # + +if [ -z "${ONE_LOCATION}" ]; then + LIB_LOCATION=/usr/lib/one +else + LIB_LOCATION=$ONE_LOCATION/lib +fi + +. $LIB_LOCATION/sh/scripts_common.sh + +error_message "Cannot import app into Docker Registry marketplace" + +exit -1 diff --git a/src/market_mad/remotes/docker_registry/import b/src/market_mad/remotes/docker_registry/import new file mode 100755 index 0000000000..95ef68deb7 --- /dev/null +++ b/src/market_mad/remotes/docker_registry/import @@ -0,0 +1,29 @@ +#!/bin/bash + +# -------------------------------------------------------------------------- # +# Copyright 2002-2021, 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. # +# -------------------------------------------------------------------------- # + +if [ -z "${ONE_LOCATION}" ]; then + LIB_LOCATION=/usr/lib/one +else + LIB_LOCATION=$ONE_LOCATION/lib +fi + +. $LIB_LOCATION/sh/scripts_common.sh + +error_message "Cannot import app into Docker Registry marketplace" + +exit -1 diff --git a/src/market_mad/remotes/docker_registry/monitor b/src/market_mad/remotes/docker_registry/monitor new file mode 100755 index 0000000000..a20b993092 --- /dev/null +++ b/src/market_mad/remotes/docker_registry/monitor @@ -0,0 +1,146 @@ +#!/usr/bin/env ruby + +# -------------------------------------------------------------------------- # +# Copyright 2002-2021, 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 'json' +require 'rexml/document' +require 'time' + +require_relative '../common/docker' + +#------------------------------------------------------------------------------- +# Class to interact and monitor Docker Private Registry +#------------------------------------------------------------------------------- +class DockerRegistryMarket + + #--------------------------------------------------------------------------- + # Default Configuration parameters for the Driver + #--------------------------------------------------------------------------- + DEFAULTS = { + :sizemb => 2048, + :fs => 'ext4', + :format => 'raw', + :agent => 'OpenNebula', + :ssl => false + } + + #--------------------------------------------------------------------------- + # Configuration varibales + # + # :sizemb -> default size for container images + # :fs -> filesystem for the image file + # :format -> for the image file, qcow2, raw + # :agent -> for HTTP client + # :ssl -> true if registry is behind SSL + #--------------------------------------------------------------------------- + def initialize(options = {}) + @options = DEFAULTS + version_path = File.dirname(__FILE__) + '/../../VERSION' + + @options.merge!(options) + + return unless File.exist?(version_path) + + @options[:agent] = "OpenNebula #{File.read(version_path)}" + end + + #--------------------------------------------------------------------------- + # Get appliance list + #--------------------------------------------------------------------------- + # + # @param url [String] Docker registry URL + def appliances(url) + appstr = '' + url = "#{url}/" unless url[-1] == '/' + rc, body = DockerMarket.get(@options, + "#{url}v2/_catalog", + @options[:ssl]) + + return [rc, body] if rc != 0 + + JSON.parse(body)['repositories'].each do |app| + regt = '' + + if !app['last_updated'].nil? + time = app['last_updated'].split('T')[0].split('-') + regt = Time.new(time[0], time[1], time[2]).to_i + end + + data = { + 'NAME' => app, + 'SOURCE' => app_url(url, app), + 'IMPORT_ID' => -1, + 'ORIGIN_ID' => -1, + 'TYPE' => 'IMAGE', + 'PUBLISHER' => 'hub.docker.com', + 'MD5' => DockerMarket.md5(@options, regt), + 'FORMAT' => 'raw', + 'VERSION' => '1.0', + 'TAGS' => '', + 'REGTIME' => regt, + 'SIZE' => @options[:sizemb], + 'DESCRIPTION' => '', + 'LINK' => "#{url}/v2/#{app}/tags/list" + } + + appstr << DockerMarket.gen_template(data) + end + + [0, appstr] + end + + # Get app URL + # + # @param url [String] Docker registry URL + # @param app [String] App name + def app_url(url, app) + url = url.gsub('http://', '') + + "docker://#{url}#{app}?size=#{@options[:sizemb]}" \ + "&filesystem=#{@options[:fs]}&format=#{@options[:format]}" + end + +end + +################################################################################ +# Main Program. Outpust the list of marketplace appliances +################################################################################ +def set_option(opt, doc, name, path) + opt[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 + + set_option(options, doc, :url, 'MARKETPLACE/TEMPLATE/BASE_URL') + set_option(options, doc, :url, 'MARKETPLACE/TEMPLATE/SSL') + set_option(options, doc, :sizemb, 'MARKETPLACE/TEMPLATE/IMAGE_SIZE_MB') + set_option(options, doc, :fs, 'MARKETPLACE/TEMPLATE/FILESYSTEM') + set_option(options, doc, :format, 'MARKETPLACE/TEMPLATE/FORMAT') + + rc, str = DockerRegistryMarket.new(options).appliances(options[:url]) + + if rc != 0 + STDERR.puts str + exit(-1) + end + + puts str +end diff --git a/src/market_mad/remotes/dockerhub/monitor b/src/market_mad/remotes/dockerhub/monitor index 55a31681af..ed3e59ac75 100755 --- a/src/market_mad/remotes/dockerhub/monitor +++ b/src/market_mad/remotes/dockerhub/monitor @@ -16,16 +16,14 @@ # limitations under the License. # # -------------------------------------------------------------------------- # -require 'net/http' -require 'uri' require 'json' -require 'base64' require 'rexml/document' require 'time' -require 'digest/md5' + +require_relative '../common/docker' #------------------------------------------------------------------------------- -# +# Class to interact and monitor DockerHub #------------------------------------------------------------------------------- class DockerHubMarket @@ -33,78 +31,32 @@ class DockerHubMarket # Default Configuration parameters for the Driver #--------------------------------------------------------------------------- DEFAULTS = { - :url => 'https://hub.docker.com/v2/repositories/library/', - :sizemb => 2048, - :fs => 'ext4', - :format => 'raw', - :agent => 'OpenNebula', + :url => 'https://hub.docker.com/v2/repositories/library/', + :sizemb => 2048, + :fs => 'ext4', + :format => 'raw', + :agent => 'OpenNebula', :page_size => 100 } - # TODO: Make configurable - TEMPLATE = " -CPU = \"1\" -MEMORY = \"768\" -GRAPHICS = [ - LISTEN =\"0.0.0.0\", - TYPE =\"vnc\" -] -CONTEXT = [ - NETWORK =\"YES\", - SSH_PUBLIC_KEY =\"$USER[SSH_PUBLIC_KEY]\", - SET_HOSTNAME =\"$NAME\" -] -OS = [ - KERNEL_CMD=\"console=ttyS0 reboot=k panic=1\" -]" - #--------------------------------------------------------------------------- # 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 + # + # :url -> of DockerHub 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 = DEFAULTS + version_path = File.dirname(__FILE__) + '/../../VERSION' + @options.merge!(options) - version_path = File.dirname(__FILE__) + '/../../VERSION' - @options[:agent] = "OpenNebula #{File.read(version_path)}" \ - if File.exist? version_path - end + return unless File.exist?(version_path) - #--------------------------------------------------------------------------- - # Fetch URL - #--------------------------------------------------------------------------- - def get(path) - # Get proxy params (needed for ruby 1.9.3) - http_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY'] - - p_host = nil - p_port = nil - - if http_proxy - p_uri = URI(http_proxy) - p_host = p_uri.host - p_port = p_uri.port - end - - uri = URI(path) - req = Net::HTTP::Get.new(uri.request_uri) - - req['User-Agent'] = @options[:agent] if @options[:agent] - - opts = { :use_ssl => true } - rc = Net::HTTP.start(uri.hostname, uri.port, - p_host, p_port, opts) do |http| - http.request(req) - end - - return [rc.code.to_i, rc.msg] unless rc.is_a? Net::HTTPSuccess - - [0, rc.body] + @options[:agent] = "OpenNebula #{File.read(version_path)}" end #--------------------------------------------------------------------------- @@ -117,7 +69,7 @@ OS = [ next_query = "#{@options[:url]}?page_size=#{@options[:page_size]}" loop do - rc, body = get(next_query) + rc, body = DockerMarket.get(@options, next_query) return [rc, body] if rc != 0 @@ -167,7 +119,7 @@ OS = [ 'ORIGIN_ID' => -1, 'TYPE' => 'IMAGE', 'PUBLISHER' => 'hub.docker.com', - 'MD5' => md5(regt), + 'MD5' => DockerMarket.md5(@options, regt), 'FORMAT' => 'raw', 'VERSION' => '1.0', 'TAGS' => '', @@ -177,19 +129,7 @@ OS = [ 'LINK' => "https://hub.docker.com/_/#{app['name']}" } - tmpl = '' - data.each {|key, val| print_var(tmpl, key, val) } - - tmpl64 = '' - print_var(tmpl64, 'DRIVER', 'raw') - print_var(tmpl64, 'DEV_PREFIX', 'vd') - - 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" + appstr << DockerMarket.gen_template(data) end [0, appstr] @@ -200,18 +140,6 @@ OS = [ "&filesystem=#{@options[:fs]}&format=#{@options[:format]}" end - 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 - end ################################################################################