diff --git a/VERSION b/VERSION index 1892b92..227cea2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.2 +2.0.0 diff --git a/resources/bin/rb_configure_initial_s3.sh b/resources/bin/rb_configure_initial_s3.sh index 86b2e25..5b2fac5 100755 --- a/resources/bin/rb_configure_initial_s3.sh +++ b/resources/bin/rb_configure_initial_s3.sh @@ -2,7 +2,6 @@ BUCKET="bucket" S3HOST="s3.service" -#ENDPOINT="https://$S3HOST" echo "INFO: Executing rb_configure_initial_s3" @@ -18,18 +17,6 @@ _RBEOF_ echo "INFO: Restarting serf. Loading new handlers" systemctl restart serf -# Generating random key and secret for minio.. -MINIO_ACCESS_KEY="`< /dev/urandom tr -dc A-Za-z0-9 | head -c20 | sed 's/ //g'`" -MINIO_SECRET_KEY="`< /dev/urandom tr -dc A-Za-z0-9 | head -c40 | sed 's/ //g'`" - -#Configure Environment Variables for minio in /etc/default/minio (used by systemd unit minio.service) -cat > /etc/default/minio <<-_RBEOF_ -MINIO_OPTS="--address :9000 --console-address :9001 --config-dir /etc/minio" -MINIO_VOLUMES=/var/minio/data -MINIO_ROOT_USER=$MINIO_ACCESS_KEY -MINIO_ROOT_PASSWORD=$MINIO_SECRET_KEY -_RBEOF_ - #Accept chef-client license chef-client --chef-license accept &>/dev/null @@ -56,43 +43,13 @@ if [ $flag -eq 0 ] ; then fi #Obtain s3 information for leader -echo "INFO: Generate /etc/redborder/s3_init_conf.yml using Minio configuration" MINIO_IP=$(serf members -tag s3=inprogress | tr ':' ' ' | awk '{print $2}') -if [ "x$MINIO_ACCESS_KEY" != "x" -a "x$MINIO_ACCESS_KEY" != "xnull" -a \ - "x$MINIO_SECRET_KEY" != "x" -a "x$MINIO_SECRET_KEY" != "xnull" ] ; then - cat > /etc/redborder/s3_init_conf.yml <<-_RBEOF_ ---- -s3: - access_key: $MINIO_ACCESS_KEY - secret_key: $MINIO_SECRET_KEY - bucket: $BUCKET - endpoint: $S3HOST -_RBEOF_ - -else - echo "ERROR: can't obtain Minio access and/or secret keys, exiting..." - exit 1 -fi - # Add s3.service name to /etc/hosts echo "INFO: Adding $S3HOST name to /etc/hosts" grep -q s3.service /etc/hosts [ $? -ne 0 -a "x$MINIO_IP" != "x" ] && echo "$MINIO_IP s3.service" >> /etc/hosts -#Create bucket -echo "INFO: Configure s3cmd to create bucket" -cat > /root/.s3cfg_initial <<-_RBEOF_ -[default] -access_key = $MINIO_ACCESS_KEY -secret_key = $MINIO_SECRET_KEY -check_ssl_certificate = False -check_ssl_hostname = False -host_base = $S3HOST -host_bucket = $S3HOST -use_https = True -_RBEOF_ - echo "INFO: Creating bucket ($BUCKET)" s3cmd -c /root/.s3cfg_initial mb s3://$BUCKET if [ $? -ne 0 ] ; then @@ -100,6 +57,5 @@ if [ $? -ne 0 ] ; then exit 1 fi - echo "INFO: S3 service configuration finished, setting serf s3=ready tag" -serf tags -set s3=ready +serf tags -set s3=ready \ No newline at end of file diff --git a/resources/bin/rb_sync_minio_cluster.sh b/resources/bin/rb_sync_minio_cluster.sh new file mode 100644 index 0000000..ccc8f0d --- /dev/null +++ b/resources/bin/rb_sync_minio_cluster.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +####################################################################### +# Copyright (c) 2024 ENEO TecnologĂ­a S.L. +# This file is part of redBorder. +# redBorder is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# redBorder is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License License for more details. +# You should have received a copy of the GNU Affero General Public License License +# along with redBorder. If not, see . +####################################################################### + +source /etc/profile.d/rvm.sh + +/usr/lib/redborder/scripts/rb_sync_minio_cluster.rb $* diff --git a/resources/scripts/rb_sync_minio_cluster.rb b/resources/scripts/rb_sync_minio_cluster.rb new file mode 100644 index 0000000..141acf6 --- /dev/null +++ b/resources/scripts/rb_sync_minio_cluster.rb @@ -0,0 +1,270 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +####################################################################### +# Copyright (c) 2024 ENEO TecnologĂ­a S.L. +# This file is part of redBorder. +# redBorder is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# redBorder is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License License for more details. +# You should have received a copy of the GNU Affero General Public License License +# along with redBorder. If not, see . +####################################################################### + +require 'net/http' +require 'uri' +require 'json' + +module RedBorder + # Add a logger for the RedBorder module + module Logger + # clean way to do puts :) + def self.log(msg) + puts msg + end + end + + # Module for initializing Minio replication + module MinioReplication + # Initializes Minio replication if the current node is the cluster leader. + # + # @return [void] + def self.init_minio_replication + return unless RedBorder::Serf.im_leader? + + RedBorder::Minio.set_minio_replicas + end + end + + # Module for interacting with Serf + module Serf + # Checks if the current node is the cluster leader. + # + # @return [Boolean] Returns true if the current node is the cluster leader, otherwise false. + def self.im_leader? + output = `serf members` + leader = '' + if $?.success? + leader_node = output.lines.find { |line| line.include?('leader=ready') } + + leader = leader_node.split[1].split(':')[0] if leader_node + end + + my_ips = `hostname -I`.split(' ') + my_ips.include? leader + end + + # Gets the name of the cluster leader. + # + # @return [String] The name of the cluster leader. + def self.cluster_leader + output = `serf members` + leader = '' + if $?.success? + leader_node = output.lines.find { |line| line.include?('leader=ready') } + + if leader_node + parts = leader_node.split + leader = parts[0] + end + end + + leader + end + end + + # Module for making HTTP requests + module HTTP + # Sends an HTTP request. + # + # @param url [String] The URL to request. + # @param method [String] The HTTP method (GET, POST, DELETE). + # @param body [String] The request body (optional). + # @param cookie [String] The cookie to include in the request (optional). + # @param log [Boolean] log response (optional). + # @return [Net::HTTPResponse] The HTTP response. + def self.request(url, method, body = nil, cookie = nil, log: false) + uri = URI.parse(url) + http = Net::HTTP.new(uri.host, uri.port) + + request_class = { 'POST' => Net::HTTP::Post, 'DELETE' => Net::HTTP::Delete }[method.upcase] || Net::HTTP::Get + request = request_class.new(uri.request_uri) + request['Content-Type'] = 'application/json' if body + request['Cookie'] = cookie if cookie + request.body = body if body + + response = http.request(request) + RedBorder::Logger.log(response.body) if log + + response + end + end + + # Module for interacting with Consul + module Consul + CONSUL_ENDPOINT = 'http://127.0.0.1:8500' + + # Retrieves S3 nodes from Consul. + # + # @return [Array] An array of hashes containing S3 node information. + # Each hash contains keys :name, :console_endpoint, and :api_endpoint. + def self.s3_nodes_from_consul + response = RedBorder::HTTP.request("#{CONSUL_ENDPOINT}/v1/catalog/service/s3", 'GET') + JSON.parse(response.body).map do |node| + { + name: node['Node'], + console_endpoint: "http://#{node['Address']}:9000", + api_endpoint: "http://#{node['Address']}:9001" + } + end + end + end + + # Module for interacting with Minio + module Minio + MINIO_CONFIG_PATH = '/etc/default/minio' + LOCAL_MINIO_ENDPOINT = 'http://127.0.0.1:9001' + MINIO_USER_KEY = 'MINIO_ROOT_USER=' + MINIO_ROOT_PASSWORD = 'MINIO_ROOT_PASSWORD=' + BUCKET = 'bucket' + MINIMUM_MINIO_HOSTS = 1 + CLEAN_S3_DEF_BODY = [{ 'path' => '/', 'versionID' => '', 'recursive' => true }].to_json + + # Retrieves the Minio session ID. + # + # @param host [String] The Minio host. + # @return [String] The Minio session ID. + def self.minio_session_id(host = LOCAL_MINIO_ENDPOINT) + credentials = RedBorder::Minio.minio_credentials + + body = { + accessKey: credentials[:accessKey], + secretKey: credentials[:secretKey] + }.to_json + + response = RedBorder::HTTP.request("#{host}/api/v1/login", 'POST', body) + response['Set-Cookie'] + end + + # Retrieves Minio credentials. + # + # @return [Hash] Minio credentials containing :accessKey and :secretKey. + def self.minio_credentials + credentials = {} + File.foreach(MINIO_CONFIG_PATH) do |line| + if line.start_with?(MINIO_USER_KEY) + credentials[:accessKey] = line.split('=').last.strip + elsif line.start_with?(MINIO_ROOT_PASSWORD) + credentials[:secretKey] = line.split('=').last.strip + end + end + credentials + end + + # Cleans S3 replication. + # + # @return [Net::HTTPResponse] The HTTP response. + def self.clean_s3_replication + RedBorder::Logger.log('Cleaning S3 replications...') + hosts = RedBorder::Consul.s3_nodes_from_consul + cookie = RedBorder::Minio.minio_session_id + names = hosts.map { |node| node[:name] } + + body = { + 'all' => true, + 'sites' => names + }.to_json + + RedBorder::HTTP.request("#{LOCAL_MINIO_ENDPOINT}/api/v1/admin/site-replication", 'DELETE', body, cookie) + end + + # Cleans S3 slave buckets. + # + # @return [void] + def self.clean_s3_slaves_buckets + RedBorder::Logger.log('Cleaning S3 Slaves Buckets...') + hosts = RedBorder::Consul.s3_nodes_from_consul + hosts.each do |host| + next if RedBorder::Serf.cluster_leader == host[:name] + + cookie = RedBorder::Minio.minio_session_id host[:api_endpoint] + + RedBorder::HTTP.request("#{host[:api_endpoint]}/api/v1/buckets/bucket/delete-objects?all_versions=true", + 'POST', CLEAN_S3_DEF_BODY, cookie) + end + end + + # Deletes S3 slave buckets. + # + # @return [void] + def self.delete_s3_slaves_buckets + RedBorder::Logger.log('Deleting S3 Slaves Buckets...') + hosts = RedBorder::Consul.s3_nodes_from_consul + hosts.each do |host| + next if RedBorder::Serf.cluster_leader == host[:name] + + cookie = RedBorder::Minio.minio_session_id host[:api_endpoint] + + body = { 'name' => BUCKET }.to_json + + RedBorder::HTTP.request("#{host[:api_endpoint]}/api/v1/buckets/#{BUCKET}", 'DELETE', body, cookie) + end + end + + # Restarts Minio. + # + # @return [void] + def self.restart + RedBorder::Logger.log('Restarting Minio Service (master)') + system('service minio restart > /dev/null 2>&1') + system('sleep 30') + end + + # Initializes cluster synchronization by performing the following steps: + # 1. Restart Minio service on the master node. + # 2. Clean S3 replication configurations. + # 3. Clean S3 buckets on slave nodes. + # 4. Delete S3 buckets on slave nodes. + # 5. Fetches information about S3 hosts from Consul. + # 6. Retrieves Minio session ID (cookie). + # 7. Retrieves Minio credentials. + # + # @return [Hash] A hash containing information about the initialized cluster synchronization. + def self.init_cluster_sync + RedBorder::Minio.restart + RedBorder::Minio.clean_s3_replication + RedBorder::Minio.clean_s3_slaves_buckets + RedBorder::Minio.delete_s3_slaves_buckets + { + hosts: RedBorder::Consul.s3_nodes_from_consul, + cookie: RedBorder::Minio.minio_session_id, + credentials: RedBorder::Minio.minio_credentials + } + end + + # Sets Minio replicas. + # + # @return [Net::HTTPResponse] The HTTP response. + def self.set_minio_replicas + cluster_data = RedBorder::Minio.init_cluster_sync + + body = cluster_data[:hosts].map do |host| + { accessKey: cluster_data[:credentials][:accessKey], secretKey: cluster_data[:credentials][:secretKey], + name: host[:name], endpoint: host[:console_endpoint] } + end.to_json + + return unless cluster_data[:hosts].size > MINIMUM_MINIO_HOSTS + + RedBorder::HTTP.request("#{LOCAL_MINIO_ENDPOINT}/api/v1/admin/site-replication", 'POST', body, + cluster_data[:cookie], log: true) + end + end +end + +# Initialize Minio replication +RedBorder::MinioReplication.init_minio_replication