diff --git a/opennebula-common/opennebula-common.post-install b/opennebula-common/opennebula-common.post-install index 8792e0e..d8d8ffc 100644 --- a/opennebula-common/opennebula-common.post-install +++ b/opennebula-common/opennebula-common.post-install @@ -45,7 +45,7 @@ if [ ! -e "${ONEHOME}/.ssh/config" ] && [ -z "$2" ]; then fi # Set permissions and owner oneadmin:cloud -for F in /var/lock/one /var/log/one /var/run/one +for F in /var/lock/one /var/log/one /var/run/one /usr/lib/one do if [ ! -d "${F}" ]; then mkdir -p "${F}" diff --git a/opennebula-node-kvm/APKBUILD b/opennebula-node-kvm/APKBUILD index b28a1b0..3ff6b88 100644 --- a/opennebula-node-kvm/APKBUILD +++ b/opennebula-node-kvm/APKBUILD @@ -9,13 +9,44 @@ arch="noarch" options="!check" license="Apache" -depends="" +depends=" + opennebula-common + libvirt-daemon + qemu-img + qemu-system-x86_64 + ruby + vlan + ipset + pciutils + rsync + tar + cronie + logrotate + augeas + libxml2 + iptables" install=" ${pkgname}.post-install ${pkgname}.post-deinstall" -source="" +source=" + /etc/cron.d/opennebula-node + /etc/init.d/opennebula-gate-proxy + /etc/logrotate.d/opennebula-gate-proxy + /etc/sudoers.d/opennebula-node-kvm + /etc/sysctl.d/bridge-nf-call.conf + /usr/bin/onegate-proxy + /usr/bin/qemu-kvm-one-gen + /usr/lib/one/onegate-proxy/onegate-proxy.rb +" package() { -} + install -Dm700 "${srcdir}/etc/cron.d/opennebula-node" "${pkgdir}/etc/cron.d/opennebula-node" + install -Dm755 "${srcdir}/etc/init.d/opennebula-gate-proxy" "${pkgdir}/etc/init.d/opennebula-gate-proxy" + install -Dm644 "${srcdir}/etc/logrotate.d/opennebula-gate-proxy" "${pkgdir}/etc/logrotate.d/opennebula-gate-proxy" + install -Dm400 "${srcdir}/etc/sudoers.d/opennebula-node-kvm" "${pkgdir}/etc/sudoers.d/opennebula-node-kvm" + install -Dm644 "${srcdir}/etc/sysctl.d/bridge-nf-call.conf" "${pkgdir}/etc/sysctl.d/bridge-nf-call.conf" + install -Dm744 "${srcdir}/usr/bin/onegate-proxy" "${pkgdir}/usr/bin/onegate-proxy" + install -Dm744 "${srcdir}/usr/bin/qemu-kvm-one-gen" "${pkgdir}/usr/bin/qemu-kvm-one-gen" + install -Dm744 -o oneadmin -g cloud "${srcdir}/usr/lib/one/onegate-proxy/onegate-proxy.rb" "${pkgdir}/usr/lib/one/onegate-proxy/onegate-proxy.rb" diff --git a/opennebula-node-kvm/etc/cron.d/opennebula-node b/opennebula-node-kvm/etc/cron.d/opennebula-node new file mode 100644 index 0000000..71d3536 --- /dev/null +++ b/opennebula-node-kvm/etc/cron.d/opennebula-node @@ -0,0 +1,4 @@ +SHELL=/bin/sh + +# activate LVM volumes on reboot +@reboot oneadmin test -x /var/tmp/one/tm/fs_lvm/activate && /var/tmp/one/tm/fs_lvm/activate >/dev/null 2>&1 diff --git a/opennebula-node-kvm/etc/init.d/opennebula-gate-proxy b/opennebula-node-kvm/etc/init.d/opennebula-gate-proxy new file mode 100644 index 0000000..be0a25c --- /dev/null +++ b/opennebula-node-kvm/etc/init.d/opennebula-gate-proxy @@ -0,0 +1,30 @@ +#!/sbin/openrc-run + +name=$RC_SVCNAME +description="OpenNebula Gate Proxy Service" +command="/usr/bin/ruby /usr/lib/one/onegate-proxy/onegate-proxy.rb" +command_user="oneadmin:cloud" +pidfile="/run/one/${RC_SVCNAME}.pid" +supervisor="supervise-daemon" + +depend() { + need opennebula + after syslog networking netmount +} + +start() { + ebegin "Starting $RC_SVCNAME" + start-stop-daemon --background \ + --exec $command \ + --make-pidfile --pidfile $pidfile --user $command_user \ + --start + eend $? +} + +stop() { + ebegin "Stopping $RC_SVCNAME" + start-stop-daemon --stop \ + --exec $command \ + --pidfile $pidfile --user $command_user + eend $? +} diff --git a/opennebula-node-kvm/etc/logrotate.d/opennebula-gate-proxy b/opennebula-node-kvm/etc/logrotate.d/opennebula-gate-proxy new file mode 100644 index 0000000..182fcea --- /dev/null +++ b/opennebula-node-kvm/etc/logrotate.d/opennebula-gate-proxy @@ -0,0 +1,11 @@ +/var/log/one/onegate-proxy.log { + delaycompress + dateext + dateformat -%Y%m%d-%s + compress + weekly + rotate 52 + missingok + notifempty + copytruncate +} diff --git a/opennebula-node-kvm/etc/sudoers.d/opennebula-node-kvm b/opennebula-node-kvm/etc/sudoers.d/opennebula-node-kvm new file mode 100644 index 0000000..86f9b01 --- /dev/null +++ b/opennebula-node-kvm/etc/sudoers.d/opennebula-node-kvm @@ -0,0 +1 @@ +oneadmin ALL=(ALL:ALL) NOPASSWD: ONE_CEPH, ONE_NET, ONE_OVS, ONE_LVM, ONE_MEM, ONE_VGPU diff --git a/opennebula-node-kvm/etc/sysctl.d/bridge-nf-call.conf b/opennebula-node-kvm/etc/sysctl.d/bridge-nf-call.conf new file mode 100644 index 0000000..d0ce3b4 --- /dev/null +++ b/opennebula-node-kvm/etc/sysctl.d/bridge-nf-call.conf @@ -0,0 +1,3 @@ +net.bridge.bridge-nf-call-arptables = 1 +net.bridge.bridge-nf-call-ip6tables = 1 +net.bridge.bridge-nf-call-iptables = 1 diff --git a/opennebula-node-kvm/opennebula-common.post-install b/opennebula-node-kvm/opennebula-common.post-install deleted file mode 100644 index 8792e0e..0000000 --- a/opennebula-node-kvm/opennebula-common.post-install +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/sh - -set -e - -ONEHOME=/var/lib/one -ONE_GROUP=cloud -ONE_USER=oneadmin -ONE_UID=9869 -ONE_GID=9869 -ONE_COMMENT="OpenNebula administrator account" - -create_cloudgroup() { - if ! getent group $ONE_GROUP > /dev/null 2>&1; then - addgroup --system --gid $ONE_GID $ONE_GROUP - fi -} - -create_oneuser() { - if ! getent passwd $ONE_USER > /dev/null 2>&1; then - adduser --system --gecos "$ONE_COMMENT" --uid $ONE_UID --ingroup $ONE_GROUP --home $ONEHOME --shell /bin/ash $ONE_USER - else - ONEHOME=`getent passwd $ONE_USER | cut -f6 -d:` - # Renable user (give him a shell) - usermod --shell /bin/ash $ONE_USER - fi - - if ! getent group disk | grep "\b$ONE_USER\b" &>/dev/null; then - usermod -a -G disk $ONE_USER - fi -} - -create_cloudgroup -create_oneuser - -# Install ~oneadmin/.ssh/config if not present on a fresh install only -if [ ! -e "${ONEHOME}/.ssh/config" ] && [ -z "$2" ]; then - if [ ! -d "${ONEHOME}/.ssh" ]; then - mkdir -p "${ONEHOME}/.ssh" - chmod 0700 "${ONEHOME}/.ssh" - chown "$ONE_USER:$ONE_GROUP" "${ONEHOME}/.ssh" - fi - cp /usr/share/one/ssh/config "${ONEHOME}/.ssh/config" - chmod 0600 "${ONEHOME}/.ssh/config" - chown "$ONE_USER:$ONE_GROUP" "${ONEHOME}/.ssh/config" -fi - -# Set permissions and owner oneadmin:cloud -for F in /var/lock/one /var/log/one /var/run/one -do - if [ ! -d "${F}" ]; then - mkdir -p "${F}" - chmod 0750 "${F}" - chown "${ONE_USER}:${ONE_GROUP}" "${F}" - fi -done - -exit 0 diff --git a/opennebula-node-kvm/opennebula-common.post-deinstall b/opennebula-node-kvm/opennebula-node-kvm.post-deinstall similarity index 100% rename from opennebula-node-kvm/opennebula-common.post-deinstall rename to opennebula-node-kvm/opennebula-node-kvm.post-deinstall diff --git a/opennebula-node-kvm/opennebula-node-kvm.post-install b/opennebula-node-kvm/opennebula-node-kvm.post-install new file mode 100644 index 0000000..49406ae --- /dev/null +++ b/opennebula-node-kvm/opennebula-node-kvm.post-install @@ -0,0 +1,140 @@ +#!/bin/sh + +set -e + +ONEHOME=/var/lib/one +ONE_GROUP=cloud +ONE_USER=oneadmin + +# Add oneadmin user into libvirt group +if getent group libvirt >/dev/null && ! getent group libvirt | cut -f4 -d: | grep -q "\<$ONE_USER\>"; then + adduser $ONE_USER libvirt +fi + +# Add oneadmin user into kvm group +if getent group kvm >/dev/null && ! getent group kvm | cut -f4 -d: | grep -q "\<$ONE_USER\>"; then + adduser $ONE_USER kvm +fi + +# Backup libvirt/QEMU configuration, reconfigure for OpenNebula +if [ -e /etc/libvirt/qemu.conf ]; then + cp -f /etc/libvirt/qemu.conf "/etc/libvirt/qemu.conf.$(date +'%Y-%m-%d_%H:%M:%S')" +fi + +AUGTOOL=$(augtool -A 2>/dev/null </dev/null || true +fi + +# # Automatically added by dh_systemd_enable/12.10ubuntu1 +# if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then +# if deb-systemd-helper debian-installed 'opennebula-gate-proxy.service'; then +# # This will only remove masks created by d-s-h on package removal. +# deb-systemd-helper unmask 'opennebula-gate-proxy.service' >/dev/null || true +# +# if deb-systemd-helper --quiet was-enabled 'opennebula-gate-proxy.service'; then +# # Create new symlinks, if any. +# deb-systemd-helper enable 'opennebula-gate-proxy.service' >/dev/null || true +# fi +# fi +# +# # Update the statefile to add new symlinks (if any), which need to be cleaned +# # up on purge. Also remove old symlinks. +# deb-systemd-helper update-state 'opennebula-gate-proxy.service' >/dev/null || true +# fi +# # End automatically added section + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +create_cloudgroup() { + if ! getent group $ONE_GROUP > /dev/null 2>&1; then + addgroup --system --gid $ONE_GID $ONE_GROUP + fi +} + +create_oneuser() { + if ! getent passwd $ONE_USER > /dev/null 2>&1; then + adduser --system --gecos "$ONE_COMMENT" --uid $ONE_UID --ingroup $ONE_GROUP --home $ONEHOME --shell /bin/ash $ONE_USER + else + ONEHOME=`getent passwd $ONE_USER | cut -f6 -d:` + # Renable user (give him a shell) + usermod --shell /bin/ash $ONE_USER + fi + + if ! getent group disk | grep "\b$ONE_USER\b" &>/dev/null; then + usermod -a -G disk $ONE_USER + fi +} + +create_cloudgroup +create_oneuser + +# Install ~oneadmin/.ssh/config if not present on a fresh install only +if [ ! -e "${ONEHOME}/.ssh/config" ] && [ -z "$2" ]; then + if [ ! -d "${ONEHOME}/.ssh" ]; then + mkdir -p "${ONEHOME}/.ssh" + chmod 0700 "${ONEHOME}/.ssh" + chown "$ONE_USER:$ONE_GROUP" "${ONEHOME}/.ssh" + fi + cp /usr/share/one/ssh/config "${ONEHOME}/.ssh/config" + chmod 0600 "${ONEHOME}/.ssh/config" + chown "$ONE_USER:$ONE_GROUP" "${ONEHOME}/.ssh/config" +fi + +# Set permissions and owner oneadmin:cloud +for F in /var/lock/one /var/log/one /var/run/one +do + if [ ! -d "${F}" ]; then + mkdir -p "${F}" + chmod 0750 "${F}" + chown "${ONE_USER}:${ONE_GROUP}" "${F}" + fi +done + +exit 0 diff --git a/opennebula-node-kvm/usr/bin/onegate-proxy b/opennebula-node-kvm/usr/bin/onegate-proxy new file mode 100755 index 0000000..ca6cf30 --- /dev/null +++ b/opennebula-node-kvm/usr/bin/onegate-proxy @@ -0,0 +1,120 @@ +#!/bin/bash + +# -------------------------------------------------------------------------- # +# Copyright 2002-2023, 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 + ONEGATE_PROXY_PID=/var/run/one/onegate-proxy.pid + ONEGATE_PROXY_SERVER=/usr/lib/one/onegate-proxy/onegate-proxy.rb + ONEGATE_PROXY_LOCK_FILE=/var/lock/one/.onegate-proxy.lock + ONEGATE_PROXY_LOG=/var/log/one/onegate-proxy.log + ONEGATE_PROXY_LOG_ERROR=/var/log/one/onegate-proxy.error +else + ONEGATE_PROXY_PID=$ONE_LOCATION/var/onegate-proxy.pid + ONEGATE_PROXY_SERVER=$ONE_LOCATION/lib/onegate-proxy/onegate-proxy.rb + ONEGATE_PROXY_LOCK_FILE=$ONE_LOCATION/var/.onegate-proxy.lock + ONEGATE_PROXY_LOG=$ONE_LOCATION/var/onegate-proxy.log + ONEGATE_PROXY_LOG_ERROR=$ONE_LOCATION/var/onegate-proxy.error +fi + +setup() +{ + if [ -f $ONEGATE_PROXY_LOCK_FILE ]; then + if [ -f $ONEGATE_PROXY_PID ]; then + ONEGATEPID=`cat $ONEGATE_PROXY_PID` + ps $ONEGATEPID &> /dev/null + if [ $? -eq 0 ]; then + echo -n "OneGate Server is still running (PID:$ONEGATEPID). Please " + echo "try 'onegate-proxy stop' first." + exit 1 + fi + fi + echo "Stale .lock detected. Erasing it." + rm $ONEGATE_PROXY_LOCK_FILE + fi +} + + +start() +{ + if [ ! -f "$ONEGATE_PROXY_SERVER" ]; then + echo "Cannot find $ONEGATE_PROXY_SERVER." + exit 1 + fi + + # Start the onegate daemon + touch $ONEGATE_PROXY_LOCK_FILE + ruby $ONEGATE_PROXY_SERVER > $ONEGATE_PROXY_LOG 2>$ONEGATE_PROXY_LOG_ERROR & + LASTPID=$! + + if [ $? -ne 0 ]; then + echo "Error executing onegate-proxy." + echo "Check $ONEGATE_PROXY_LOG_ERROR and $ONEGATE_PROXY_LOG for more information" + exit 1 + else + echo $LASTPID > $ONEGATE_PROXY_PID + fi + + sleep 1 + ps $LASTPID &> /dev/null + + if [ $? -ne 0 ]; then + echo "Error executing onegate-proxy." + echo "Check $ONEGATE_PROXY_LOG_ERROR and $ONEGATE_PROXY_LOG for more information" + exit 1 + fi + + echo "onegate-proxy started" +} + +# +# Function that stops the daemon/service +# +stop() +{ + if [ ! -f $ONEGATE_PROXY_PID ]; then + echo "Couldn't find onegate-proxy process pid." + exit 1 + fi + + # Kill the onegate daemon + kill -INT `cat $ONEGATE_PROXY_PID` &> /dev/null + + # Remove pid files + rm -f $ONEGATE_PROXY_LOCK_FILE &> /dev/null + rm -f $ONEGATE_PROXY_PID &> /dev/null + + echo "onegate-proxy stopped" +} + +case "$1" in + start) + setup + start + ;; + stop) + stop + ;; + restart) + stop + setup + start + ;; + *) + echo "Usage: onegate-proxy {start|stop|restart}" >&2 + exit 3 + ;; +esac diff --git a/opennebula-node-kvm/usr/bin/qemu-kvm-one-gen b/opennebula-node-kvm/usr/bin/qemu-kvm-one-gen new file mode 100755 index 0000000..3e12a97 --- /dev/null +++ b/opennebula-node-kvm/usr/bin/qemu-kvm-one-gen @@ -0,0 +1,120 @@ +#!/bin/sh + +# -------------------------------------------------------------------------- # +# Copyright 2002-2023, 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. # +#--------------------------------------------------------------------------- # + +set -e + +CMD=$(basename ${0}) + +# file path to the new qemu-kvm symlink +QEMU_ONE="/usr/bin/qemu-kvm-one" + +# +# functions +# + +print_usage() +{ + cat <&2 + print_usage >&2 + exit 1 + ;; +esac + +# find cpu arch or default to x86_64 +if command -v arch >/dev/null 2>&1 ; then + ARCH=$(arch) +else + ARCH="x86_64" +fi + +# verify that symlink is not already created +if [ -L "${QEMU_ONE}" ] ; then + # symlink already exists + + qemu_target=$(readlink "${QEMU_ONE}") + + if [ -e "${qemu_target}" ] && [ -z "${FORCE_CREATE}" ] ; then + # symlink is valid + exit 0 + fi +elif [ -e "${QEMU_ONE}" ] ; then + # there is a file of the same name and it is not a symlink + + if [ -z "${FORCE_CREATE}" ] ; then + echo "ERROR: ${CMD}: File '${QEMU_ONE}' already exists but it is not a symlink !" >&2 + exit 1 + else + # --force is used + rm -f "${QEMU_ONE}" + fi +fi + +# search the known paths for qemu binary +# +# NOTE: you can add new supported paths here in the future +for QEMU_BIN in \ + /usr/libexec/qemu-kvm \ + /usr/bin/qemu-kvm \ + /usr/bin/qemu-system-${ARCH} \ + ; +do + if [ -e "${QEMU_BIN}" ] ; then + ln -s ${FORCE_CREATE:+-f} "${QEMU_BIN}" "${QEMU_ONE}" + exit 0 + fi +done + +# no qemu binary found -> we signal error and exit +echo "ERROR: ${CMD}: No qemu kvm binary found !" >&2 +exit 1 diff --git a/opennebula-node-kvm/usr/lib/one/onegate-proxy/onegate-proxy.rb b/opennebula-node-kvm/usr/lib/one/onegate-proxy/onegate-proxy.rb new file mode 100644 index 0000000..0c4a1cc --- /dev/null +++ b/opennebula-node-kvm/usr/lib/one/onegate-proxy/onegate-proxy.rb @@ -0,0 +1,309 @@ +#!/usr/bin/env ruby +# -------------------------------------------------------------------------- # +# Copyright 2002-2023, 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. # +#--------------------------------------------------------------------------- # + +# frozen_string_literal: true + +# rubocop:disable Lint/MissingCopEnableDirective +# rubocop:disable Lint/RedundantRequireStatement +# rubocop:disable Layout/FirstHashElementIndentation +# rubocop:disable Layout/HashAlignment +# rubocop:disable Layout/HeredocIndentation +# rubocop:disable Layout/IndentationWidth +# rubocop:disable Style/HashSyntax +# rubocop:disable Style/ParallelAssignment + +ONE_LOCATION = ENV['ONE_LOCATION'] + +if !ONE_LOCATION + RUBY_LIB_LOCATION = '/usr/lib/one/ruby' + GEMS_LOCATION = '/usr/share/one/gems' + ETC_LOCATION = '/etc/one' + REMOTES_LOCATION = '/var/tmp/one' +else + RUBY_LIB_LOCATION = ONE_LOCATION + '/lib/ruby' + GEMS_LOCATION = ONE_LOCATION + '/share/gems' + ETC_LOCATION = ONE_LOCATION + '/etc' + REMOTES_LOCATION = ONE_LOCATION + '/var/remotes' +end + +CONFIGURATION_FILE = REMOTES_LOCATION + '/etc/onegate-proxy.conf' + +# %%RUBYGEMS_SETUP_BEGIN%% +if File.directory?(GEMS_LOCATION) + real_gems_path = File.realpath(GEMS_LOCATION) + if !defined?(Gem) || Gem.path != [real_gems_path] + $LOAD_PATH.reject! {|p| p =~ /vendor_ruby/ } + + # Suppress warnings from Rubygems + # https://github.com/OpenNebula/one/issues/5379 + begin + verb = $VERBOSE + $VERBOSE = nil + require 'rubygems' + Gem.use_paths(real_gems_path) + ensure + $VERBOSE = verb + end + end +end +# %%RUBYGEMS_SETUP_END%% + +$LOAD_PATH << RUBY_LIB_LOCATION + +require 'async/io' +require 'async/io/stream' +require 'async/io/trap' +require 'etc' +require 'pp' +require 'rb-inotify' +require 'socket' +require 'yaml' + +$stdout.sync = true +$stderr.sync = true + +DEFAULT_OPTIONS = { + :debug_level => 2, # 0 = ERROR, 1 = WARNING, 2 = INFO, 3 = DEBUG + :process_owner => 'oneadmin', + :onegate_addr => '127.0.0.1', + :onegate_port => '5030', + :service_addr => '169.254.16.9' +}.freeze + +# Proxy-class for converting log levels between OpenNebula and +# the socketry/console library. It also splits specific log levels +# into separate stdout and stderr loggers. +class Logger + + LOG_LEVEL_MAP = { + 0 => '3', # ERROR + 1 => '2', # WARN + 2 => '1', # INFO + 3 => '0' # DEBUG + }.freeze + + def initialize(log_level = 2) + @out = Console::Logger.default_logger $stdout, { + 'CONSOLE_LEVEL' => LOG_LEVEL_MAP[log_level] + } + @err = Console::Logger.default_logger $stderr, { + 'CONSOLE_LEVEL' => LOG_LEVEL_MAP[log_level] + } + end + + # rubocop:disable Style/ArgumentsForwarding + def error(*args, &block) + @err.error(*args, &block) + end + + def warn(*args, &block) + @err.warn(*args, &block) + end + + def info(*args, &block) + @out.info(*args, &block) + end + + def debug(*args, &block) + @err.debug(*args, &block) + end + # rubocop:enable Style/ArgumentsForwarding + +end + +# Class that implements a classic two-way TCP socket proxy (async). +class OneGateProxy + + def initialize(options = {}) + @options = DEFAULT_OPTIONS.dup.merge! options + @options.each {|k, v| instance_variable_set("@#{k}", v) } + + @logger = Logger.new options[:debug_level] + + @sigint = Async::IO::Trap.new :INT + @sigint.install! + + @inotify = setup_inotify + @inotify_io = Async::IO::Generic.new @inotify.to_io + + @proxy_ep = Async::IO::Endpoint.socket setup_socket + end + + def run + # NOTE: At this point all config should be set in stone, + # we can drop root privileges.. + drop_privileges + + Async do |task| + # Make CTRL-C work.. + task.async do + @sigint.wait { exit 0 } + end + + # Handle filesystem notifications.. + task.async do + @inotify.process while @inotify_io.wait_readable + end + + glue_peers task + end + end + + private + + def drop_privileges + new_gid, new_uid = Etc.getpwnam(@process_owner).gid, + Etc.getpwnam(@process_owner).uid + + @logger.info(self) do + "Drop root privileges -> #{@process_owner}" + end + + Process::Sys.setgid new_gid + Process::Sys.setuid new_uid + end + + def setup_inotify + inotify = INotify::Notifier.new + inotify.watch(CONFIGURATION_FILE, :modify) do + @logger.info(self) do + "#{CONFIGURATION_FILE} has been just updated, exiting.." + end + # We assume here that the service will be restarted by + # the service manager. + exit 0 + end + inotify + rescue Errno::ENOENT => e + @logger.error(self) do + e.message + end + # We assume here that the service will be restarted by + # the service manager. + exit e.class::Errno + end + + def setup_service_addr + # NOTE: We need the service_addr to be defined on one of the interfaces + # inside the host, one natural choice is the loopback interface (lo). + # Effectively we set it once, subsequent restarts of the service should + # honor the idempotence. + ip_address_add_cmd = lambda do |cidr_host, nic_device| + check = "[ -n \"$(ip a s to '#{cidr_host}' dev '#{nic_device}')\" ]" + apply = "ip a a '#{cidr_host}' dev '#{nic_device}'" + "#{check.strip} >/dev/null 2>&1 || #{apply.strip}" + end + system ip_address_add_cmd.call "#{@service_addr}/32", 'lo' + end + + def setup_socket(listen = Socket::SOMAXCONN) + # NOTE: Must be executed before calling bind(), otherwise it fails.. + setup_service_addr + + sock = Socket.new Socket::AF_INET, Socket::SOCK_STREAM, 0 + sock.setsockopt Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1 + + @logger.info(self) do + "Bind #{Addrinfo.tcp(@service_addr, @onegate_port).inspect}" + end + + sock.bind Socket.pack_sockaddr_in(@onegate_port, @service_addr) + sock.listen listen + sock + end + + def glue_streams(stream1, stream2, task) + Async do + concurrent = [] + concurrent << task.async do + while (chunk = stream1.read_partial) + stream2.write chunk + stream2.flush + end + end + concurrent << task.async do + while (chunk = stream2.read_partial) + stream1.write chunk + stream1.flush + end + end + concurrent.each(&:wait) + end + end + + def glue_peers(task) + @proxy_ep.accept do |vm_peer| + @logger.debug(self) do + "Accept #{vm_peer.remote_address.inspect}" + end + + begin + gate_ep = Async::IO::Endpoint.tcp @onegate_addr, + @onegate_port + gate_ep.connect do |gate_peer| + vm_stream, gate_stream = Async::IO::Stream.new(vm_peer), + Async::IO::Stream.new(gate_peer) + + glue_streams(vm_stream, gate_stream, task).wait + + @logger.debug(self) do + "Close #{gate_peer.remote_address.inspect}" + end + + gate_peer.close + end + rescue Errno::ECONNREFUSED, + Errno::ECONNRESET, + Errno::EHOSTUNREACH, + Errno::ETIMEDOUT => e + @logger.error(self) do + e.message + end + end + + @logger.debug(self) do + "Close #{vm_peer.remote_address.inspect}" + end + + vm_peer.close + end + end + +end + +if caller.empty? + options = DEFAULT_OPTIONS.dup + + # NOTE: The "CONFIGURATION_FILE" is updated during the host sync procedure. + begin + options.merge! YAML.load_file(CONFIGURATION_FILE) + rescue StandardError => e + warn "Error parsing config file #{CONFIGURATION_FILE}: #{e.message}" + exit 1 + end + + puts <<~HEADER + -------------------------------------- + Proxy configuration + -------------------------------------- + #{options.pretty_inspect.strip} + -------------------------------------- + HEADER + + service = OneGateProxy.new options + service.run +end