diff --git a/Makefile b/Makefile index 1504074b..7463a381 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,9 @@ packer-service_vnf: packer-alpine318 ${DIR_EXPORT}/service_vnf.qcow2 packer-service_wordpress: packer-alma8 ${DIR_EXPORT}/service_wordpress.qcow2 @${INFO} "Packer service_wordpress done" +packer-service_VRouter: packer-alpine318 ${DIR_EXPORT}/service_VRouter.qcow2 + @${INFO} "Packer service_VRouter done" + packer-service_OneKE: packer-ubuntu2204 ${DIR_EXPORT}/service_OneKE.qcow2 @${INFO} "Packer service_OneKE done" diff --git a/Makefile.config b/Makefile.config index 37df420c..1b498f70 100644 --- a/Makefile.config +++ b/Makefile.config @@ -21,7 +21,7 @@ DISTROS := alma8 alma9 \ rocky8 rocky9 \ ubuntu2004 ubuntu2004min ubuntu2204 ubuntu2204min -SERVICES := service_vnf service_wordpress service_OneKE service_OneKEa +SERVICES := service_vnf service_wordpress service_VRouter service_OneKE service_OneKEa .DEFAULT_GOAL := help diff --git a/appliances/lib/artifacts/vnf/onekea-2.2.0/kea-hook-onelease4-1.1.1-r0.apk b/appliances/VRouter/DHCP4/kea-hook-onelease4-1.1.1-r0.apk similarity index 100% rename from appliances/lib/artifacts/vnf/onekea-2.2.0/kea-hook-onelease4-1.1.1-r0.apk rename to appliances/VRouter/DHCP4/kea-hook-onelease4-1.1.1-r0.apk diff --git a/appliances/VRouter/DHCP4/main.rb b/appliances/VRouter/DHCP4/main.rb new file mode 100644 index 00000000..f483bb8e --- /dev/null +++ b/appliances/VRouter/DHCP4/main.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +require 'json' +require_relative '../vrouter.rb' + +module Service +module DHCP4 + extend self + + DEPENDS_ON = %w[Service::Failover] + + ONEAPP_VNF_DHCP4_ENABLED = env :ONEAPP_VNF_DHCP4_ENABLED, 'NO' + + ONEAPP_VNF_DHCP4_AUTHORITATIVE = env :ONEAPP_VNF_DHCP4_AUTHORITATIVE, 'YES' + + ONEAPP_VNF_DHCP4_MAC2IP_ENABLED = env :ONEAPP_VNF_DHCP4_MAC2IP_ENABLED, 'YES' + ONEAPP_VNF_DHCP4_MAC2IP_MACPREFIX = env :ONEAPP_VNF_DHCP4_MAC2IP_MACPREFIX, '02:00' + + ONEAPP_VNF_DHCP4_LEASE_TIME = env :ONEAPP_VNF_DHCP4_LEASE_TIME, '3600' + + ONEAPP_VNF_DHCP4_GATEWAY = env :ONEAPP_VNF_DHCP4_GATEWAY, nil + ONEAPP_VNF_DHCP4_DNS = env :ONEAPP_VNF_DHCP4_DNS, nil + + ONEAPP_VNF_DHCP4_INTERFACES = env :ONEAPP_VNF_DHCP4_INTERFACES, '' # nil -> none, empty -> all + + def parse_env + @interfaces ||= parse_interfaces ONEAPP_VNF_DHCP4_INTERFACES + @mgmt ||= detect_mgmt_interfaces + + interfaces = @interfaces.keys - @mgmt + + @n2a ||= addrs_to_nics(interfaces, family: %w[inet]).to_h do |a, n| + [n.first, a] + end + + @a2s ||= addrs_to_subnets(interfaces, family: %w[inet]).to_h do |a, s| + [a.split('/').first, s] + end + + @s2r ||= subnets_to_ranges(@a2s.values) + + interfaces.each_with_object({}) do |nic, vars| + p = env("ONEAPP_VNF_DHCP4_#{nic.upcase}", nil)&.split(':')&.map(&:strip) + + vars[nic] = { + address: @n2a[nic], + subnet: if p.nil? then @a2s[@n2a[nic]] else p[0] end, + range: if p.nil? then @s2r[@a2s[@n2a[nic]]] else p[1] end, + gateway: env("ONEAPP_VNF_DHCP4_#{nic.upcase}_GATEWAY", ONEAPP_VNF_DHCP4_GATEWAY), + dns: env("ONEAPP_VNF_DHCP4_#{nic.upcase}_DNS", ONEAPP_VNF_DHCP4_DNS), + mtu: env("ONEAPP_VNF_DHCP4_#{nic.upcase}_MTU", ip_link_show(nic)['mtu']), + } + end + end + + def install(initdir: '/etc/init.d') + msg :info, 'DHCP4::install' + + onelease4_apk = File.join File.dirname(__FILE__), 'kea-hook-onelease4-1.1.1-r0.apk' + + puts bash <<~SCRIPT + apk --no-cache add ruby kea-dhcp4 + apk --no-cache --allow-untrusted add '#{onelease4_apk}' + SCRIPT + + file "#{initdir}/one-dhcp4", <<~SERVICE, mode: 'u=rwx,g=rx,o=' + #!/sbin/openrc-run + + source /run/one-context/one_env + + command="/usr/bin/ruby" + command_args="-r /etc/one-appliance/lib/helpers.rb -r #{__FILE__}" + + output_log="/var/log/one-appliance/one-dhcp4.log" + error_log="/var/log/one-appliance/one-dhcp4.log" + + depend() { + after net firewall keepalived + } + + start_pre() { + rc-service kea-dhcp4 start --nodeps + } + + start() { :; } + + stop() { :; } + + stop_post() { + rc-service kea-dhcp4 stop --nodeps + } + SERVICE + + toggle [:update] + end + + def configure(basedir: '/etc/kea') + msg :info, 'DHCP4::configure' + + if ONEAPP_VNF_DHCP4_ENABLED + dhcp4_vars = parse_env + + config = { 'Dhcp4' => { + 'interfaces-config' => { 'interfaces' => dhcp4_vars.keys }, + 'authoritative' => ONEAPP_VNF_DHCP4_AUTHORITATIVE, + 'option-data' => [], + 'subnet4' => dhcp4_vars.map do |nic, vars| + data = [] + data << { 'name' => 'routers', 'data' => vars[:gateway] } unless vars[:gateway].nil? + data << { 'name' => 'domain-name-servers', 'data' => vars[:dns] } unless vars[:dns].nil? + data << { 'name' => 'interface-mtu', 'data' => vars[:mtu].to_s } unless vars[:mtu].nil? || nic == 'lo' + { 'subnet' => vars[:subnet], + 'pools' => [ { 'pool' => vars[:range] } ], + 'option-data' => data, + 'reservations' => [ + { 'flex-id' => "'DO-NOT-LEASE-#{vars[:address]}'", + 'ip-address' => vars[:address] } ], + 'reservation-mode' => 'all' } + end, + 'lease-database' => { + 'type' => 'memfile', + 'persist' => true, + 'lfc-interval' => 2 * ONEAPP_VNF_DHCP4_LEASE_TIME.to_i + }, + 'sanity-checks' => { 'lease-checks' => 'fix-del' }, + 'valid-lifetime' => ONEAPP_VNF_DHCP4_LEASE_TIME.to_i, + 'calculate-tee-times' => true, + 'loggers' => [ + { 'name' => 'kea-dhcp4', + 'output_options' => [ { 'output' => '/var/log/kea/kea-dhcp4.log' } ], + 'severity' => 'INFO', + 'debuglevel' => 0 } + ], + 'hooks-libraries' => if ONEAPP_VNF_DHCP4_MAC2IP_ENABLED then + [ { 'library' => '/usr/lib/kea/hooks/libkea-onelease-dhcp4.so', + 'parameters' => { + 'enabled' => true, + 'byte-prefix' => ONEAPP_VNF_DHCP4_MAC2IP_MACPREFIX, + 'logger-name' => 'onelease-dhcp4', + 'debug' => false, + 'debug-logfile' => '/var/log/kea/onelease-dhcp4-debug.log' } } ] + else [] end + } } + + file "#{basedir}/kea-dhcp4.conf", JSON.pretty_generate(config), owner: 'kea', + group: 'kea', + mode: 'u=rw,g=r,o=', + overwrite: true + toggle [:enable] + else + toggle [:stop, :disable] + end + end + + def toggle(operations) + operations.each do |op| + msg :debug, "DHCP4::toggle([:#{op}])" + case op + when :reload + puts bash 'rc-service kea-dhcp4 reload' + when :enable + puts bash 'rc-update add kea-dhcp4 default' + puts bash 'rc-update add one-dhcp4 default' + when :disable + puts bash 'rc-update del kea-dhcp4 default ||:' + puts bash 'rc-update del one-dhcp4 default ||:' + when :update + puts bash 'rc-update -u' + else + puts bash "rc-service one-dhcp4 #{op.to_s}" + end + end + end + + def bootstrap + msg :info, 'DHCP4::bootstrap' + end +end +end diff --git a/appliances/VRouter/DHCP4/tests.rb b/appliances/VRouter/DHCP4/tests.rb new file mode 100644 index 00000000..af57bf95 --- /dev/null +++ b/appliances/VRouter/DHCP4/tests.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require 'rspec' + +def clear_env + ENV.delete_if { |name| name.include?('VROUTER_') || name.include?('_VNF_') } +end + +def clear_vars(object) + object.instance_variables.each { |name| object.remove_instance_variable(name) } +end + +RSpec.describe self do + it 'should provide and parse all env vars' do + clear_env + + ENV['ONEAPP_VNF_DHCP4_ENABLED'] = 'YES' + ENV['ONEAPP_VNF_DHCP4_AUTHORITATIVE'] = 'YES' + + ENV['ONEAPP_VNF_DHCP4_MAC2IP_ENABLED'] = 'YES' + ENV['ONEAPP_VNF_DHCP4_MAC2IP_MACPREFIX'] = '02:00' + + ENV['ONEAPP_VNF_DHCP4_LEASE_TIME'] = '3600' + + ENV['ONEAPP_VNF_DHCP4_GATEWAY'] = '1.2.3.4' + ENV['ONEAPP_VNF_DHCP4_DNS'] = '1.1.1.1' + + ENV['ONEAPP_VNF_DHCP4_INTERFACES'] = 'lo/127.0.0.1 eth0 eth1 eth2 eth3' + ENV['ETH0_VROUTER_MANAGEMENT'] = 'YES' + + ENV['ONEAPP_VNF_DHCP4_ETH2'] = '30.0.0.0/8:30.40.50.64-30.40.50.68' + ENV['ONEAPP_VNF_DHCP4_ETH2_GATEWAY'] = '30.40.50.1' + ENV['ONEAPP_VNF_DHCP4_ETH2_DNS'] = '8.8.8.8' + + ENV['ONEAPP_VNF_DHCP4_ETH3_GATEWAY'] = '40.50.60.1' + ENV['ONEAPP_VNF_DHCP4_ETH3_DNS'] = '8.8.4.4' + + load './main.rb'; include Service::DHCP4 + + allow(Service::DHCP4).to receive(:ip_addr_list).and_return([ + { 'ifname' => 'lo', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '127.0.0.1', + 'prefixlen' => 8 } ] }, + + { 'ifname' => 'eth0', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '10.20.30.40', + 'prefixlen' => 24 } ] }, + + { 'ifname' => 'eth1', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '20.30.40.50', + 'prefixlen' => 16 } ] }, + + { 'ifname' => 'eth2', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '30.40.50.60', + 'prefixlen' => 8 } ] }, + + { 'ifname' => 'eth3', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '40.50.60.70', + 'prefixlen' => 24 } ] }, + ]) + + allow(Service::DHCP4).to receive(:ip_link_show).and_return( + { 'mtu' => 1111 }, + { 'mtu' => 2222 }, + { 'mtu' => 3333 }, + { 'mtu' => 4444 }, + ) + + clear_vars Service::DHCP4 + + expect(Service::DHCP4.parse_env).to eq ({ + 'lo' => { + address: '127.0.0.1', + dns: '1.1.1.1', + gateway: '1.2.3.4', + mtu: 1111, + range: '127.0.0.2-127.255.255.254', + subnet: '127.0.0.0/8', + }, + 'eth1' => { + address: '20.30.40.50', + dns: '1.1.1.1', + gateway: '1.2.3.4', + mtu: 2222, + range: '20.30.0.2-20.30.255.254', + subnet: '20.30.0.0/16', + }, + 'eth2' => { + address: '30.40.50.60', + dns: '8.8.8.8', + gateway: '30.40.50.1', + mtu: 3333, + range: '30.40.50.64-30.40.50.68', + subnet: '30.0.0.0/8', + }, + 'eth3' => { + address: '40.50.60.70', + dns: '8.8.4.4', + gateway: '40.50.60.1', + mtu: 4444, + range: '40.50.60.2-40.50.60.254', + subnet: '40.50.60.0/24', + }, + }) + end +end diff --git a/appliances/VRouter/DNS/main.rb b/appliances/VRouter/DNS/main.rb new file mode 100644 index 00000000..d93a8c4d --- /dev/null +++ b/appliances/VRouter/DNS/main.rb @@ -0,0 +1,341 @@ +# frozen_string_literal: true + +require 'erb' +require_relative '../vrouter.rb' + +module Service +module DNS + extend self + + DEPENDS_ON = %w[Service::Failover] + + ONEAPP_VNF_DNS_ENABLED = env :ONEAPP_VNF_DNS_ENABLED, 'NO' + ONEAPP_VNF_DNS_TCP_DISABLED = env :ONEAPP_VNF_DNS_TCP_DISABLED, 'NO' + ONEAPP_VNF_DNS_UDP_DISABLED = env :ONEAPP_VNF_DNS_UDP_DISABLED, 'NO' + + ONEAPP_VNF_DNS_UPSTREAM_TIMEOUT = env :ONEAPP_VNF_DNS_UPSTREAM_TIMEOUT, 1128 + ONEAPP_VNF_DNS_MAX_CACHE_TTL = env :ONEAPP_VNF_DNS_MAX_CACHE_TTL, 3600 + + ONEAPP_VNF_DNS_USE_ROOTSERVERS = env :ONEAPP_VNF_DNS_USE_ROOTSERVERS, 'YES' + ONEAPP_VNF_DNS_NAMESERVERS = env :ONEAPP_VNF_DNS_NAMESERVERS, '' + + ONEAPP_VNF_DNS_INTERFACES = env :ONEAPP_VNF_DNS_INTERFACES, '' # nil -> none, empty -> all + ONEAPP_VNF_DNS_ALLOWED_NETWORKS = env :ONEAPP_VNF_DNS_ALLOWED_NETWORKS, '' + + def parse_env + @interfaces ||= parse_interfaces ONEAPP_VNF_DNS_INTERFACES + @mgmt ||= detect_mgmt_interfaces + + interfaces = @interfaces.keys - @mgmt + + @n2a ||= addrs_to_nics(interfaces, family: %w[inet]).to_h do |a, n| + [n.first, a] + end + + { + interfaces: @interfaces.select do |nic, _| + interfaces.include?(nic) + end.to_h do |nic, info| + info[:addr] = @n2a[nic] + [nic, info] + end, + + nameservers: ONEAPP_VNF_DNS_NAMESERVERS.split(%r{[ ,;]}) + .map(&:strip) + .reject(&:empty?), + + networks: if ONEAPP_VNF_DNS_ALLOWED_NETWORKS.empty? then + addrs_to_subnets(interfaces, family: %w[inet]).values.join(%[,]) + else + ONEAPP_VNF_DNS_ALLOWED_NETWORKS + end.split(%r{[ ,;]}) + .map(&:strip) + .reject(&:empty?) + } + end + + def install(initdir: '/etc/init.d') + msg :info, 'DNS::install' + + puts bash <<~SCRIPT + apk --no-cache add dns-root-hints ruby unbound + install -o unbound -g unbound -m u=rwx,go=rx -d /var/log/unbound/ + SCRIPT + + file "#{initdir}/one-dns", <<~SERVICE, mode: 'u=rwx,g=rx,o=' + #!/sbin/openrc-run + + source /run/one-context/one_env + + command="/usr/bin/ruby" + command_args="-r /etc/one-appliance/lib/helpers.rb -r #{__FILE__}" + + output_log="/var/log/one-appliance/one-dns.log" + error_log="/var/log/one-appliance/one-dns.log" + + depend() { + after net firewall keepalived + } + + start_pre() { + rc-service unbound start --nodeps + } + + start() { :; } + + stop() { :; } + + stop_post() { + rc-service unbound stop --nodeps + } + SERVICE + + toggle [:update] + end + + def configure(basedir: '/etc/unbound') + msg :info, 'DNS::configure' + + if ONEAPP_VNF_DNS_ENABLED + proto_yesno = ->(proto) { + udp = ONEAPP_VNF_DNS_UDP_DISABLED ? 'no' : 'yes' + tcp = ONEAPP_VNF_DNS_TCP_DISABLED ? 'no' : 'yes' + case proto + when 'udp', 'udp-upstream' then udp + when 'tcp' then tcp + when 'tcp-upstream' then udp == 'yes' ? 'no' : 'yes' + end + } + + dns_vars = parse_env + + file "#{basedir}/unbound.conf", ERB.new(<<~CONFIG, trim_mode: '-').result(binding), mode: 'u=rw,g=r,o=', overwrite: true + server: + # verbosity number, 0 is least verbose. 1 is default. + verbosity: 1 + + # specify the interfaces to answer queries from by ip-address. + # The default is to listen to localhost (127.0.0.1 and ::1). + # specify 0.0.0.0 and ::0 to bind to all available interfaces. + # specify every interface[@port] on a new 'interface:' labelled line. + # The listen interfaces are not changed on reload, only on restart. + # LOCALHOST: + interface: 127.0.0.1 + interface: ::1 + # ALL: + # interface: 0.0.0.0 + # interface: ::0 + # WHITELIST: + <%- dns_vars[:interfaces].each do |_, info| -%> + interface: <%= render_interface(info, name: false) %> + <%- end -%> + + # port to answer queries from + # port: 53 + + # specify the interfaces to send outgoing queries to authoritative + # server from by ip-address. If none, the default (all) interface + # is used. Specify every interface on a 'outgoing-interface:' line. + # outgoing-interface: 192.0.2.153 + # outgoing-interface: 2001:DB8::5 + # outgoing-interface: 2001:DB8::6 + + # msec for waiting for an unknown server to reply. Increase if you + # are behind a slow satellite link, to eg. 1128. + unknown-server-time-limit: <%= ONEAPP_VNF_DNS_UPSTREAM_TIMEOUT %> + + # Enable IPv4, "yes" or "no". + do-ip4: yes + + # Enable IPv6, "yes" or "no". + do-ip6: yes + + # Enable UDP, "yes" or "no". + do-udp: <%= proto_yesno.('udp') %> + + # Enable TCP, "yes" or "no". + do-tcp: <%= proto_yesno.('tcp') %> + + # upstream connections use TCP only (and no UDP), "yes" or "no" + # useful for tunneling scenarios, default no. + tcp-upstream: <%= proto_yesno.('tcp-upstream') %> + + # upstream connections also use UDP (even if do-udp is no). + # useful if if you want UDP upstream, but don't provide UDP downstream. + udp-upstream-without-downstream: <%= proto_yesno.('udp-upstream') %> + + # control which clients are allowed to make (recursive) queries + # to this server. Specify classless netblocks with /size and action. + # By default everything is refused, except for localhost. + # Choose deny (drop message), refuse (polite error reply), + # allow (recursive ok), allow_setrd (recursive ok, rd bit is forced on), + # allow_snoop (recursive and nonrecursive ok) + # deny_non_local (drop queries unless can be answered from local-data) + # refuse_non_local (like deny_non_local but polite error reply). + # DEFAULT RULES: + access-control: 0.0.0.0/0 refuse + access-control: ::0/0 refuse + access-control: 127.0.0.0/8 allow + access-control: ::1 allow + access-control: ::ffff:127.0.0.1 allow + # WHITELIST: + <%- dns_vars[:networks].each do |subnet| -%> + access-control: <%= subnet %> allow + <%- end -%> + + # the time to live (TTL) value lower bound, in seconds. Default 0. + # If more than an hour could easily give trouble due to stale data. + cache-min-ttl: 0 + + # the time to live (TTL) value cap for RRsets and messages in the + # cache. Items are not cached for longer. In seconds. + cache-max-ttl: <%= ONEAPP_VNF_DNS_MAX_CACHE_TTL %> + + # TODO: chroot + # if given, a chroot(2) is done to the given directory. + # i.e. you can chroot to the working directory, for example, + # for extra security, but make sure all files are in that directory. + # + # If chroot is enabled, you should pass the configfile (from the + # commandline) as a full path from the original root. After the + # chroot has been performed the now defunct portion of the config + # file path is removed to be able to reread the config after a reload. + # + # All other file paths (working dir, logfile, roothints, and + # key files) can be specified in several ways: + # o as an absolute path relative to the new root. + # o as a relative path to the working directory. + # o as an absolute path relative to the original root. + # In the last case the path is adjusted to remove the unused portion. + # + # The pid file can be absolute and outside of the chroot, it is + # written just prior to performing the chroot and dropping permissions. + # + # Additionally, unbound may need to access /dev/urandom (for entropy). + # How to do this is specific to your OS. + # + # If you give "" no chroot is performed. The path must not end in a /. + # chroot: "" + + # if given, user privileges are dropped (after binding port), + # and the given username is assumed. Default is user "unbound". + # If you give "" no privileges are dropped. + # username: "unbound" + + # the working directory. The relative files in this config are + # relative to this directory. If you give "" the working directory + # is not changed. + # If you give a server: directory: dir before include: file statements + # then those includes can be relative to the working directory. + # directory: "" + + # the log file, "" means log to stderr. + # Use of this option sets use-syslog to "no". + logfile: "/var/log/unbound/unbound.log" + + # Log to syslog(3) if yes. The log facility LOG_DAEMON is used to + # log to. If yes, it overrides the logfile. + # use-syslog: yes + + # Log identity to report. if empty, defaults to the name of argv[0] + # (usually "unbound"). + log-identity: "" + + # print UTC timestamp in ascii to logfile, default is epoch in seconds. + log-time-ascii: yes + + # print one line with time, IP, name, type, class for every query. + log-queries: no + + # print one line per reply, with time, IP, name, type, class, rcode, + # timetoresolve, fromcache and responsesize. + log-replies: no + + # print log lines that say why queries return SERVFAIL to clients. + log-servfail: yes + + # file to read root hints from. + # get one from https://www.internic.net/domain/named.cache + <%- if ONEAPP_VNF_DNS_USE_ROOTSERVERS -%> + root-hints: /usr/share/dns-root-hints/named.root + <%- else -%> + # root-hints: /usr/share/dns-root-hints/named.root + <%- end -%> + + # enable to not answer id.server and hostname.bind queries. + hide-identity: yes + + # enable to not answer version.server and version.bind queries. + hide-version: yes + + # Serve expired responses from cache, with TTL 0 in the response, + # and then attempt to fetch the data afresh. + serve-expired: no + + # Use systemd socket activation for UDP, TCP, and control sockets. + use-systemd: no + + # Detach from the terminal, run in background, "yes" or "no". + # Set the value to "no" when unbound runs as systemd service. + do-daemonize: yes + + # Remote control config section. + remote-control: + control-enable: no + + # Forward zones + # Create entries like below, to make all queries for 'example.com' and + # 'example.org' go to the given list of servers. These servers have to handle + # recursion to other nameservers. List zero or more nameservers by hostname + # or by ipaddress. Use an entry with name "." to forward all queries. + # If you enable forward-first, it attempts without the forward if it fails. + # forward-zone: + # name: "example.com" + # forward-addr: 192.0.2.68 + # forward-addr: 192.0.2.73@5355 # forward to port 5355. + # forward-first: no + # forward-tls-upstream: no + # forward-no-cache: no + # forward-zone: + # name: "example.org" + # forward-host: fwd.example.com + <%- unless ONEAPP_VNF_DNS_USE_ROOTSERVERS -%> + forward-zone: + name: "." + <%- dns_vars[:nameservers].each do |nameserver| -%> + forward-addr: <%= nameserver %> + <%- end -%> + <%- end -%> + CONFIG + + toggle [:enable] + else + toggle [:stop, :disable] + end + end + + def toggle(operations) + operations.each do |op| + msg :debug, "DNS::toggle([:#{op}])" + case op + when :reload + puts bash 'rc-service unbound reload' + when :enable + puts bash 'rc-update add unbound default' + puts bash 'rc-update add one-dns default' + when :disable + puts bash 'rc-update del unbound default ||:' + puts bash 'rc-update del one-dns default ||:' + when :update + puts bash 'rc-update -u' + else + puts bash "rc-service one-dns #{op.to_s}" + end + end + end + + def bootstrap + msg :info, 'DNS::bootstrap' + end +end +end diff --git a/appliances/VRouter/DNS/tests.rb b/appliances/VRouter/DNS/tests.rb new file mode 100644 index 00000000..df2019d6 --- /dev/null +++ b/appliances/VRouter/DNS/tests.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require 'rspec' + +def clear_env + ENV.delete_if { |name| name.include?('VROUTER_') || name.include?('_VNF_') } +end + +def clear_vars(object) + object.instance_variables.each { |name| object.remove_instance_variable(name) } +end + +RSpec.describe self do + it 'should provide and parse all env vars (default networks)' do + clear_env + + ENV['ONEAPP_VNF_DNS_ENABLED'] = 'YES' + ENV['ONEAPP_VNF_DNS_TCP_DISABLED'] = 'YES' + ENV['ONEAPP_VNF_DNS_UDP_DISABLED'] = 'YES' + + ENV['ONEAPP_VNF_DNS_UPSTREAM_TIMEOUT'] = '123' + ENV['ONEAPP_VNF_DNS_MAX_CACHE_TTL'] = '234' + + ENV['ONEAPP_VNF_DNS_USE_ROOTSERVERS'] = 'YES' + ENV['ONEAPP_VNF_DNS_NAMESERVERS'] = '1.1.1.1 8.8.8.8' + + ENV['ONEAPP_VNF_DNS_INTERFACES'] = 'eth0 eth1 eth2 eth3' + ENV['ETH0_VROUTER_MANAGEMENT'] = 'YES' + + ENV['ONEAPP_VNF_DNS_ALLOWED_NETWORKS'] = '' + + load './main.rb'; include Service::DNS + + allow(Service::DNS).to receive(:ip_addr_list).and_return([ + { 'ifname' => 'eth0', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '10.20.30.40', + 'prefixlen' => 24 } ] }, + + { 'ifname' => 'eth1', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '20.30.40.50', + 'prefixlen' => 16 } ] }, + + { 'ifname' => 'eth2', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '30.40.50.60', + 'prefixlen' => 8 } ] }, + + { 'ifname' => 'eth3', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '40.50.60.70', + 'prefixlen' => 24 } ] }, + ]) + + clear_vars Service::DNS + + expect(Service::DNS.parse_env).to eq ({ + interfaces: { 'eth1' => { addr: '20.30.40.50', name: 'eth1', port: nil }, + 'eth2' => { addr: '30.40.50.60', name: 'eth2', port: nil }, + 'eth3' => { addr: '40.50.60.70', name: 'eth3', port: nil } }, + + nameservers: %w[1.1.1.1 8.8.8.8], + + networks: %w[20.30.0.0/16 30.0.0.0/8 40.50.60.0/24], + }) + end + + it 'should provide and parse all env vars' do + clear_env + + ENV['ONEAPP_VNF_DNS_ENABLED'] = 'YES' + ENV['ONEAPP_VNF_DNS_TCP_DISABLED'] = 'YES' + ENV['ONEAPP_VNF_DNS_UDP_DISABLED'] = 'YES' + + ENV['ONEAPP_VNF_DNS_UPSTREAM_TIMEOUT'] = '123' + ENV['ONEAPP_VNF_DNS_MAX_CACHE_TTL'] = '234' + + ENV['ONEAPP_VNF_DNS_USE_ROOTSERVERS'] = 'YES' + ENV['ONEAPP_VNF_DNS_NAMESERVERS'] = '1.1.1.1 8.8.8.8' + + ENV['ONEAPP_VNF_DNS_INTERFACES'] = 'eth0 eth1 eth2 eth3' + ENV['ETH0_VROUTER_MANAGEMENT'] = 'YES' + + ENV['ONEAPP_VNF_DNS_ALLOWED_NETWORKS'] = '20.30.0.0/16 30.0.0.0/8' + + load './main.rb'; include Service::DNS + + allow(Service::DNS).to receive(:ip_addr_list).and_return([ + { 'ifname' => 'eth0', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '10.20.30.40', + 'prefixlen' => 24 } ] }, + + { 'ifname' => 'eth1', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '20.30.40.50', + 'prefixlen' => 16 } ] }, + + { 'ifname' => 'eth2', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '30.40.50.60', + 'prefixlen' => 8 } ] }, + + { 'ifname' => 'eth3', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '40.50.60.70', + 'prefixlen' => 24 } ] }, + ]) + + clear_vars Service::DNS + + expect(Service::DNS.parse_env).to eq ({ + interfaces: { 'eth1' => { addr: '20.30.40.50', name: 'eth1', port: nil }, + 'eth2' => { addr: '30.40.50.60', name: 'eth2', port: nil }, + 'eth3' => { addr: '40.50.60.70', name: 'eth3', port: nil } }, + + nameservers: %w[1.1.1.1 8.8.8.8], + + networks: %w[20.30.0.0/16 30.0.0.0/8], + }) + end +end diff --git a/appliances/VRouter/Failover/execute.rb b/appliances/VRouter/Failover/execute.rb new file mode 100644 index 00000000..2b049830 --- /dev/null +++ b/appliances/VRouter/Failover/execute.rb @@ -0,0 +1,192 @@ +# frozen_string_literal: false + +require 'json' + +module Service +module Failover + extend self + + VROUTER_ID = env :VROUTER_ID, nil + + SERVICES = { + 'one-router4' => { _ENABLED: 'ONEAPP_VNF_ROUTER4_ENABLED', + fallback: VROUTER_ID.nil? ? 'NO' : 'YES' }, + + 'one-nat4' => { _ENABLED: 'ONEAPP_VNF_NAT4_ENABLED', + fallback: 'NO' }, + + 'one-lvs' => { _ENABLED: 'ONEAPP_VNF_LB_ENABLED', + fallback: 'NO' }, + + 'one-haproxy' => { _ENABLED: 'ONEAPP_VNF_HAPROXY_ENABLED', + fallback: 'NO' }, + + 'one-sdnat4' => { _ENABLED: 'ONEAPP_VNF_SDNAT4_ENABLED', + fallback: 'NO' }, + + 'one-dns' => { _ENABLED: 'ONEAPP_VNF_DNS_ENABLED', + fallback: 'NO' }, + + 'one-dhcp4' => { _ENABLED: 'ONEAPP_VNF_DHCP4_ENABLED', + fallback: 'NO' } + } + + FIFO_PATH = '/run/keepalived/vrrp_notify_fifo.sock' + STATE_PATH = '/run/one-failover.state' + + STATE_TO_DIRECTION = { + 'BACKUP' => :down, + 'DELETED' => :down, + 'FAULT' => :down, + 'MASTER' => :up, + 'STOP' => :down, + nil => :stay + } + def to_event(line) + k = [:type, :name, :state, :priority] + v = line.strip.split.map(&:strip).map{|s| s.delete_prefix('"').delete_suffix('"')} + k.zip(v).to_h + end + + def to_task(event) + event[:state].upcase! + + state = load_state + state[:state].upcase! + + if event[:type] != 'GROUP' + direction = :stay + ignored = true + else + if STATE_TO_DIRECTION[event[:state]] == STATE_TO_DIRECTION[state[:state]] + direction = :stay + ignored = false + else + direction = STATE_TO_DIRECTION[event[:state]] + ignored = false + end + save_state event[:state] + end + + { event: event, from: state[:state], to: event[:state], direction: direction, ignored: ignored } + end + + def save_state(state, state_path = STATE_PATH) + content = JSON.fast_generate({ state: state }) + File.open state_path, File::CREAT | File::TRUNC | File::WRONLY do |f| + f.flock File::LOCK_EX + f.write content + end + end + + def load_state(state_path = STATE_PATH) + content = File.open state_path, File::RDONLY do |f| + f.flock File::LOCK_EX + f.read + end + JSON.parse content, symbolize_names: true + rescue Errno::ENOENT + { state: 'UNKNOWN' } + end + + def process_events(fifo_path = FIFO_PATH) + loop do + begin + File.open fifo_path, File::RDONLY do |f| + f.each do |line| + event = to_event line + task = to_task event + msg :info, task + method(task[:direction]).call + end + end + rescue StandardError => e + msg :error, e.full_message + + # NOTE: We always disable all services on fatal errors + # to avoid any potential conflicts. + down + next + ensure + sleep 1 + end + end + end + + def execute + msg :info, 'Failover::execute' + process_events + end + + def update_conf_d(path, key, value) + File.open path, File::CREAT | File::RDWR, 0644 do |f| + f.flock File::LOCK_EX + content = f.read.lines.map(&:strip) + if line = content.find { |line| line =~ /^[#\s]*#{key}\s*=/ } + if value.nil? + line.replace %[##{line}] unless line.start_with?(%[#]) + else + line.replace %[#{key}="#{value}"] + end + else + content << %[#{key}="#{value}"] + end + f.rewind + f.puts content.join(%[\n]) + f.flush + f.truncate f.pos + end + end + + def stay + msg :debug, :STAY + end + + def up + msg :debug, :UP + + load_env + + SERVICES.each do |service, settings| + enabled = env settings[:_ENABLED], settings[:fallback] + + msg :debug, "#{service}(#{enabled ? ':enabled' : ':disabled'})" + + if enabled + update_conf_d "/etc/conf.d/#{service}", 'rc_need', nil + + bash <<~SCRIPT, terminate: false + rc-update -u ||: + rc-service #{service} restart ||: + SCRIPT + else + update_conf_d "/etc/conf.d/#{service}", 'rc_need', 'THIS-SERVICE-IS-MASKED' + + bash <<~SCRIPT, terminate: false + rc-update -u ||: + rc-service #{service} stop ||: + SCRIPT + end + + sleep 1 + end + end + + def down + msg :debug, :DOWN + + SERVICES.each do |service, _| + msg :debug, "#{service}(:disabled)" + + update_conf_d "/etc/conf.d/#{service}", 'rc_need', 'THIS-SERVICE-IS-MASKED' + + bash <<~SCRIPT, terminate: false + rc-update -u ||: + rc-service #{service} stop ||: + SCRIPT + + sleep 1 + end + end +end +end diff --git a/appliances/VRouter/Failover/main.rb b/appliances/VRouter/Failover/main.rb new file mode 100644 index 00000000..3f8cc9a9 --- /dev/null +++ b/appliances/VRouter/Failover/main.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require_relative 'execute.rb' + +module Service +module Failover + extend self + + DEPENDS_ON = %w[Service::Keepalived] + + def install(initdir: '/etc/init.d') + msg :info, 'Failover::install' + + puts bash 'apk --no-cache add ruby' + + file "#{initdir}/one-failover", <<~SERVICE, mode: 'u=rwx,g=rx,o=' + #!/sbin/openrc-run + + source /run/one-context/one_env + + command="/usr/bin/ruby" + command_args="-r /etc/one-appliance/lib/helpers.rb -r #{__FILE__} -e Service::Failover.execute" + + command_background="yes" + pidfile="/run/$RC_SVCNAME.pid" + + output_log="/var/log/one-appliance/one-failover.log" + error_log="/var/log/one-appliance/one-failover.log" + + depend() { + need keepalived + after net + } + SERVICE + + toggle [:disable, :update, :stop] + end + + def configure + msg :info, 'Failover::configure' + + puts bash <<~SCRIPT + if [[ "$(virt-what)" != vmware ]]; then + rc-update del open-vm-tools default && rc-update -u ||: + fi + SCRIPT + + toggle [:enable, :start] + end + + def toggle(operations) + operations.each do |op| + msg :info, "Failover::toggle([:#{op}])" + case op + when :enable + puts bash 'rc-update add one-failover default' + when :disable + puts bash 'rc-update del one-failover default ||:' + when :update + puts bash 'rc-update -u' + else + puts bash "rc-service one-failover #{op.to_s}" + end + end + end + + def bootstrap + msg :info, 'Failover::bootstrap' + end +end +end diff --git a/appliances/VRouter/HAProxy/execute.rb b/appliances/VRouter/HAProxy/execute.rb new file mode 100644 index 00000000..b23374b8 --- /dev/null +++ b/appliances/VRouter/HAProxy/execute.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'erb' +require_relative '../vrouter.rb' + +module Service +module HAProxy + extend self + + VROUTER_ID = env :VROUTER_ID, nil + + def extract_backends(objects = {}) + static = backends.from_env(prefix: 'ONEAPP_VNF_HAPROXY_LB') + + dynamic = VROUTER_ID.nil? ? backends.from_vms(objects, prefix: 'ONEGATE_HAPROXY_LB') + : backends.from_vnets(objects, prefix: 'ONEGATE_HAPROXY_LB') + + # NOTE: This ensures that backends can be added dynamically only to statically defined LBs. + merged = hashmap.combine static, backends.intersect(static, dynamic) + + # Replace all "" placeholders where possible. + backends.resolve_vips merged + end + + def render_servers_cfg(haproxy_vars, basedir: '/etc/haproxy') + @interfaces ||= parse_interfaces ONEAPP_VNF_HAPROXY_INTERFACES + @mgmt ||= detect_mgmt_interfaces + @addrs ||= addrs_to_nics(@interfaces.keys - @mgmt, family: %[inet]).keys + + file "#{basedir}/servers.cfg", ERB.new(<<~SERVERS, trim_mode: '-').result(binding), mode: 'u=rw,g=r,o=', overwrite: true + <%- haproxy_vars[:by_endpoint]&.each do |(lb_idx, ip, port), servers| -%> + <%- if @addrs.include?(ip) -%> + frontend lb<%= lb_idx %>_<%= port %> + mode tcp + bind <%= ip %>:<%= port %> + default_backend lb<%= lb_idx %>_<%= port %> + + backend lb<%= lb_idx %>_<%= port %> + mode tcp + balance roundrobin + option tcp-check + <%- servers&.values&.each do |s| -%> + server lb<%= lb_idx %>_<%= s[:host] %>_<%= s[:port] %> <%= s[:host] %>:<%= s[:port] %> check observe layer4 error-limit 50 on-error mark-down + <% end %> + <%- end -%> + <%- end -%> + SERVERS + end + + def execute(basedir: '/etc/haproxy') + msg :info, 'HAProxy::execute' + + # Handle "static" load-balancers. + render_servers_cfg extract_backends, basedir: basedir + toggle [:reload] + + if ONEAPP_VNF_HAPROXY_ONEGATE_ENABLED + prev = [] + + get_objects = VROUTER_ID.nil? ? :get_service_vms : :get_vrouter_vnets + + loop do + unless (objects = method(get_objects).call).empty? + if prev != (this = extract_backends(objects)) + msg :debug, this + + render_servers_cfg this, basedir: basedir + + toggle [:reload] + end + + prev = this + end + + sleep ONEAPP_VNF_HAPROXY_REFRESH_RATE.to_i + end + else + sleep + end + end +end +end diff --git a/appliances/VRouter/HAProxy/main.rb b/appliances/VRouter/HAProxy/main.rb new file mode 100644 index 00000000..9cd1d5dc --- /dev/null +++ b/appliances/VRouter/HAProxy/main.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require_relative '../vrouter.rb' +require_relative 'execute.rb' + +module Service +module HAProxy + extend self + + DEPENDS_ON = %w[Service::Failover] + + ONEAPP_VNF_HAPROXY_ENABLED = env :ONEAPP_VNF_HAPROXY_ENABLED, 'NO' + ONEAPP_VNF_HAPROXY_ONEGATE_ENABLED = env :ONEAPP_VNF_HAPROXY_ONEGATE_ENABLED, 'NO' + + ONEAPP_VNF_HAPROXY_REFRESH_RATE = env :ONEAPP_VNF_HAPROXY_REFRESH_RATE, '30' + + ONEAPP_VNF_HAPROXY_INTERFACES = env :ONEAPP_VNF_HAPROXY_INTERFACES, '' # nil -> none, empty -> all + + def install(initdir: '/etc/init.d') + msg :info, 'HAProxy::install' + + puts bash 'apk --no-cache add haproxy ruby' + + file "#{initdir}/one-haproxy", <<~SERVICE, mode: 'u=rwx,g=rx,o=' + #!/sbin/openrc-run + + source /run/one-context/one_env + + command="/usr/bin/ruby" + command_args="-r /etc/one-appliance/lib/helpers.rb -r #{__FILE__} -e Service::HAProxy.execute" + + command_background="yes" + pidfile="/run/$RC_SVCNAME.pid" + + output_log="/var/log/one-appliance/one-haproxy.log" + error_log="/var/log/one-appliance/one-haproxy.log" + + depend() { + after net keepalived + } + + start_pre() { + rc-service haproxy start --nodeps + } + + stop_post() { + rc-service haproxy stop --nodeps + } + SERVICE + + toggle [:update] + end + + def configure(basedir: '/etc/haproxy', confdir: '/etc/conf.d') + msg :info, 'HAProxy::configure' + + if ONEAPP_VNF_HAPROXY_ENABLED + file "#{confdir}/haproxy", <<~CONFIG, mode: 'u=rw,g=r,o=', overwrite: true + HAPROXY_CONF="#{basedir}" + CONFIG + + file "#{basedir}/haproxy.cfg", <<~CONFIG, mode: 'u=rw,g=r,o=', overwrite: true + global + log /dev/log local0 + log /dev/log local1 notice + stats socket /var/run/haproxy.sock mode 666 level admin + stats timeout 120s + user haproxy + group haproxy + daemon + + defaults + log global + retries 3 + maxconn 2000 + timeout connect 5s + timeout client 120s + timeout server 120s + CONFIG + + toggle [:enable] + else + toggle [:stop, :disable] + end + end + + def toggle(operations) + operations.each do |op| + msg :debug, "HAProxy::toggle([:#{op}])" + case op + when :reload + puts bash 'rc-service haproxy start' + puts bash 'rc-service haproxy reload' + when :enable + puts bash 'rc-update add haproxy default' + puts bash 'rc-update add one-haproxy default' + when :disable + puts bash 'rc-update del haproxy default ||:' + puts bash 'rc-update del one-haproxy default ||:' + when :update + puts bash 'rc-update -u' + else + puts bash "rc-service one-haproxy #{op.to_s}" + end + end + end + + def bootstrap + msg :info, 'HAProxy::bootstrap' + end +end +end diff --git a/appliances/VRouter/HAProxy/tests.rb b/appliances/VRouter/HAProxy/tests.rb new file mode 100644 index 00000000..8e861e39 --- /dev/null +++ b/appliances/VRouter/HAProxy/tests.rb @@ -0,0 +1,400 @@ +# frozen_string_literal: true + +require 'json' +require 'rspec' +require 'tmpdir' + +def clear_env + ENV.delete_if { |name| name.include?('VROUTER_') || name.include?('_HAPROXY') } +end + +def clear_vars(object) + object.instance_variables.each { |name| object.remove_instance_variable(name) } +end + +RSpec.describe self do + it 'should provide and parse all env vars (static)' do + clear_env + + ENV['ONEAPP_VNF_HAPROXY_ENABLED'] = 'YES' + + ENV['ONEAPP_VNF_HAPROXY_REFRESH_RATE'] = '' + + ENV['ONEAPP_VNF_HAPROXY_LB0_IP'] = '10.2.10.69' + ENV['ONEAPP_VNF_HAPROXY_LB0_PORT'] = '1234' + + ENV['ONEAPP_VNF_HAPROXY_LB0_SERVER0_HOST'] = '10.2.100.10' + ENV['ONEAPP_VNF_HAPROXY_LB0_SERVER0_PORT'] = '12345' + + ENV['ONEAPP_VNF_HAPROXY_LB0_SERVER1_HOST'] = '10.2.100.20' + ENV['ONEAPP_VNF_HAPROXY_LB0_SERVER1_PORT'] = '12345' + + ENV['ONEAPP_VNF_HAPROXY_LB1_IP'] = '10.2.20.69' + ENV['ONEAPP_VNF_HAPROXY_LB1_PORT'] = '4321' + + ENV['ONEAPP_VNF_HAPROXY_LB1_SERVER0_HOST'] = '10.2.200.10' + ENV['ONEAPP_VNF_HAPROXY_LB1_SERVER0_PORT'] = '54321' + + ENV['ONEAPP_VNF_HAPROXY_LB1_SERVER1_HOST'] = '10.2.200.20' + ENV['ONEAPP_VNF_HAPROXY_LB1_SERVER1_PORT'] = '54321' + + load './main.rb'; include Service::HAProxy + + expect(Service::HAProxy::ONEAPP_VNF_HAPROXY_ENABLED).to be true + expect(Service::HAProxy::ONEAPP_VNF_HAPROXY_REFRESH_RATE).to eq '30' + + Service::HAProxy.const_set :VROUTER_ID, '86' + + allow(Service::HAProxy).to receive(:detect_nics).and_return(%w[eth0 eth1 eth2 eth3]) + + expect(Service::HAProxy.extract_backends).to eq({ + by_endpoint: { + [ 0, '10.2.10.69', '1234' ] => + { [ '10.2.100.10', '12345' ] => { host: '10.2.100.10', port: '12345' }, + [ '10.2.100.20', '12345' ] => { host: '10.2.100.20', port: '12345' } }, + + [ 1, '10.2.20.69', '4321' ] => + { [ '10.2.200.10', '54321' ] => { host: '10.2.200.10', port: '54321' }, + [ '10.2.200.20', '54321' ] => { host: '10.2.200.20', port: '54321' } } }, + + options: { 0 => { ip: '10.2.10.69', port: '1234' }, + 1 => { ip: '10.2.20.69', port: '4321' } } + }) + end + + it 'should render servers.cfg (static)' do + clear_env + + ENV['ONEAPP_VNF_HAPROXY_ENABLED'] = 'YES' + + ENV['ONEAPP_VNF_HAPROXY_REFRESH_RATE'] = '' + + ENV['ONEAPP_VNF_HAPROXY_LB0_IP'] = '10.2.10.69' + ENV['ONEAPP_VNF_HAPROXY_LB0_PORT'] = '1234' + + ENV['ONEAPP_VNF_HAPROXY_LB0_SERVER0_HOST'] = '10.2.100.10' + ENV['ONEAPP_VNF_HAPROXY_LB0_SERVER0_PORT'] = '12345' + + ENV['ONEAPP_VNF_HAPROXY_LB0_SERVER1_HOST'] = '10.2.100.20' + ENV['ONEAPP_VNF_HAPROXY_LB0_SERVER1_PORT'] = '12345' + + ENV['ONEAPP_VNF_HAPROXY_LB1_IP'] = '10.2.10.69' + ENV['ONEAPP_VNF_HAPROXY_LB1_PORT'] = '4321' + + ENV['ONEAPP_VNF_HAPROXY_LB1_SERVER0_HOST'] = '10.2.200.10' + ENV['ONEAPP_VNF_HAPROXY_LB1_SERVER0_PORT'] = '54321' + + ENV['ONEAPP_VNF_HAPROXY_LB1_SERVER1_HOST'] = '10.2.200.20' + ENV['ONEAPP_VNF_HAPROXY_LB1_SERVER1_PORT'] = '54321' + + load './main.rb'; include Service::HAProxy + + Service::HAProxy.const_set :VROUTER_ID, '86' + + allow(Service::HAProxy).to receive(:toggle).and_return(nil) + allow(Service::HAProxy).to receive(:sleep).and_return(nil) + allow(Service::HAProxy).to receive(:detect_nics).and_return(%w[eth0 eth1 eth2 eth3]) + allow(Service::HAProxy).to receive(:addrs_to_nics).and_return({ + '10.2.10.69' => ['eth0'] + }) + + clear_vars Service::HAProxy + + output = <<~STATIC + frontend lb0_1234 + mode tcp + bind 10.2.10.69:1234 + default_backend lb0_1234 + + backend lb0_1234 + mode tcp + balance roundrobin + option tcp-check + server lb0_10.2.100.10_12345 10.2.100.10:12345 check observe layer4 error-limit 50 on-error mark-down + server lb0_10.2.100.20_12345 10.2.100.20:12345 check observe layer4 error-limit 50 on-error mark-down + + frontend lb1_4321 + mode tcp + bind 10.2.10.69:4321 + default_backend lb1_4321 + + backend lb1_4321 + mode tcp + balance roundrobin + option tcp-check + server lb1_10.2.200.10_54321 10.2.200.10:54321 check observe layer4 error-limit 50 on-error mark-down + server lb1_10.2.200.20_54321 10.2.200.20:54321 check observe layer4 error-limit 50 on-error mark-down + STATIC + + Dir.mktmpdir do |dir| + Service::HAProxy.execute basedir: dir + result = File.read "#{dir}/servers.cfg" + expect(result.strip).to eq output.strip + end + end + + it 'should render servers.cfg (dynamic)' do + clear_env + + ENV['ONEAPP_VNF_HAPROXY_ENABLED'] = 'YES' + ENV['ONEAPP_VNF_HAPROXY_ONEGATE_ENABLED'] = 'YES' + + ENV['ONEAPP_VNF_HAPROXY_REFRESH_RATE'] = '' + + ENV['ONEAPP_VNF_HAPROXY_LB0_IP'] = '10.2.11.86' + ENV['ONEAPP_VNF_HAPROXY_LB0_PORT'] = '6969' + + ENV['ONEAPP_VNF_HAPROXY_LB0_SERVER0_HOST'] = '10.2.11.200' + ENV['ONEAPP_VNF_HAPROXY_LB0_SERVER0_PORT'] = '1234' + + ENV['ONEAPP_VNF_HAPROXY_LB0_SERVER1_HOST'] = '10.2.11.201' + ENV['ONEAPP_VNF_HAPROXY_LB0_SERVER1_PORT'] = '1234' + + ENV['ONEAPP_VNF_HAPROXY_LB1_IP'] = '10.2.11.86' + ENV['ONEAPP_VNF_HAPROXY_LB1_PORT'] = '8686' + + (vnets ||= []) << JSON.parse(<<~'VNET0') + { + "VNET": { + "ID": "0", + "AR_POOL": { + "AR": [ + { + "AR_ID": "0", + "LEASES": { + "LEASE": [ + { + "IP": "10.2.11.202", + "MAC": "02:00:0a:02:0b:ca", + "VM": "167", + "NIC_NAME": "NIC0", + "BACKEND": "YES", + + "ONEGATE_HAPROXY_LB0_IP": "10.2.11.86", + "ONEGATE_HAPROXY_LB0_PORT": "6969", + "ONEGATE_HAPROXY_LB0_SERVER_HOST": "10.2.11.202", + "ONEGATE_HAPROXY_LB0_SERVER_PORT": "1234", + "ONEGATE_HAPROXY_LB0_SERVER_WEIGHT": "1" + }, + { + "IP": "10.2.11.201", + "MAC": "02:00:0a:02:0b:c9", + "VM": "167", + "NIC_NAME": "NIC0", + "BACKEND": "YES", + + "ONEGATE_HAPROXY_LB0_IP": "10.2.11.86", + "ONEGATE_HAPROXY_LB0_PORT": "6969", + "ONEGATE_HAPROXY_LB0_SERVER_HOST": "10.2.11.201", + "ONEGATE_HAPROXY_LB0_SERVER_PORT": "1234", + "ONEGATE_HAPROXY_LB0_SERVER_WEIGHT": "1", + + "ONEGATE_HAPROXY_LB1_IP": "10.2.11.86", + "ONEGATE_HAPROXY_LB1_PORT": "8686", + "ONEGATE_HAPROXY_LB1_SERVER_HOST": "10.2.11.201", + "ONEGATE_HAPROXY_LB1_SERVER_PORT": "4321", + "ONEGATE_HAPROXY_LB1_SERVER_WEIGHT": "1" + }, + { + "IP": "10.2.11.200", + "MAC": "02:00:0a:02:0b:c8", + "VM": "167", + "NIC_NAME": "NIC0", + "BACKEND": "YES", + + "ONEGATE_HAPROXY_LB0_IP": "10.2.11.86", + "ONEGATE_HAPROXY_LB0_PORT": "6969", + "ONEGATE_HAPROXY_LB0_SERVER_HOST": "10.2.11.200", + "ONEGATE_HAPROXY_LB0_SERVER_PORT": "1234", + "ONEGATE_HAPROXY_LB0_SERVER_WEIGHT": "1", + + "ONEGATE_HAPROXY_LB1_IP": "10.2.11.86", + "ONEGATE_HAPROXY_LB1_PORT": "8686", + "ONEGATE_HAPROXY_LB1_SERVER_HOST": "10.2.11.200", + "ONEGATE_HAPROXY_LB1_SERVER_PORT": "4321", + "ONEGATE_HAPROXY_LB1_SERVER_WEIGHT": "1" + } + ] + } + } + ] + } + } + } + VNET0 + + load './main.rb'; include Service::HAProxy + + Service::HAProxy.const_set :VROUTER_ID, '86' + + allow(Service::HAProxy).to receive(:detect_nics).and_return(%w[eth0 eth1 eth2 eth3]) + allow(Service::HAProxy).to receive(:addrs_to_nics).and_return({ + '10.2.11.86' => ['eth0'] + }) + + clear_vars Service::HAProxy + + output = <<~'DYNAMIC' + frontend lb0_6969 + mode tcp + bind 10.2.11.86:6969 + default_backend lb0_6969 + + backend lb0_6969 + mode tcp + balance roundrobin + option tcp-check + server lb0_10.2.11.200_1234 10.2.11.200:1234 check observe layer4 error-limit 50 on-error mark-down + server lb0_10.2.11.201_1234 10.2.11.201:1234 check observe layer4 error-limit 50 on-error mark-down + server lb0_10.2.11.202_1234 10.2.11.202:1234 check observe layer4 error-limit 50 on-error mark-down + + frontend lb1_8686 + mode tcp + bind 10.2.11.86:8686 + default_backend lb1_8686 + + backend lb1_8686 + mode tcp + balance roundrobin + option tcp-check + server lb1_10.2.11.201_4321 10.2.11.201:4321 check observe layer4 error-limit 50 on-error mark-down + server lb1_10.2.11.200_4321 10.2.11.200:4321 check observe layer4 error-limit 50 on-error mark-down + DYNAMIC + + Dir.mktmpdir do |dir| + haproxy_vars = Service::HAProxy.extract_backends vnets + Service::HAProxy.render_servers_cfg haproxy_vars, basedir: dir + result = File.read "#{dir}/servers.cfg" + expect(result.strip).to eq output.strip + end + end + + it 'should render servers.cfg (dynamic/OneFlow)' do + clear_env + + ENV['ONEAPP_VNF_HAPROXY_ENABLED'] = 'YES' + ENV['ONEAPP_VNF_HAPROXY_ONEGATE_ENABLED'] = 'YES' + + ENV['ONEAPP_VNF_HAPROXY_REFRESH_RATE'] = '' + + ENV['ONEAPP_VNF_HAPROXY_LB0_IP'] = '10.2.11.86' + ENV['ONEAPP_VNF_HAPROXY_LB0_PORT'] = '5432' + + (vms ||= []) << JSON.parse(<<~'VM0') + { + "VM": { + "NAME": "server_0_(service_23)", + "ID": "435", + "STATE": "3", + "LCM_STATE": "3", + "USER_TEMPLATE": { + "HOT_RESIZE": { + "CPU_HOT_ADD_ENABLED": "NO", + "MEMORY_HOT_ADD_ENABLED": "NO" + }, + "LOGO": "images/logos/linux.png", + "LXD_SECURITY_PRIVILEGED": "true", + "MEMORY_UNIT_COST": "MB", + "ONEGATE_HAPROXY_LB0_IP": "10.2.11.86", + "ONEGATE_HAPROXY_LB0_PORT": "5432", + "ONEGATE_HAPROXY_LB0_SERVER_HOST": "10.2.11.202", + "ONEGATE_HAPROXY_LB0_SERVER_PORT": "2345", + "ROLE_NAME": "server", + "SERVICE_ID": "23" + }, + "TEMPLATE": { + "NIC": [ + { + "IP": "10.2.11.202", + "MAC": "02:00:0a:02:0b:ca", + "NAME": "_NIC0", + "NETWORK": "service" + }, + { + "IP": "172.20.0.122", + "MAC": "02:00:ac:14:00:7a", + "NAME": "_NIC1", + "NETWORK": "private" + } + ], + "NIC_ALIAS": [] + } + } + } + VM0 + (vms ||= []) << JSON.parse(<<~'VM1') + { + "VM": { + "NAME": "server_1_(service_23)", + "ID": "436", + "STATE": "3", + "LCM_STATE": "3", + "USER_TEMPLATE": { + "HOT_RESIZE": { + "CPU_HOT_ADD_ENABLED": "NO", + "MEMORY_HOT_ADD_ENABLED": "NO" + }, + "LOGO": "images/logos/linux.png", + "LXD_SECURITY_PRIVILEGED": "true", + "MEMORY_UNIT_COST": "MB", + "ONEGATE_HAPROXY_LB0_IP": "10.2.11.86", + "ONEGATE_HAPROXY_LB0_PORT": "5432", + "ONEGATE_HAPROXY_LB0_SERVER_HOST": "10.2.11.203", + "ONEGATE_HAPROXY_LB0_SERVER_PORT": "2345", + "ROLE_NAME": "server", + "SERVICE_ID": "23" + }, + "TEMPLATE": { + "NIC": [ + { + "IP": "10.2.11.203", + "MAC": "02:00:0a:02:0b:cb", + "NAME": "_NIC0", + "NETWORK": "service" + }, + { + "IP": "172.20.0.123", + "MAC": "02:00:ac:14:00:7b", + "NAME": "_NIC1", + "NETWORK": "private" + } + ], + "NIC_ALIAS": [] + } + } + } + VM1 + + load './main.rb'; include Service::HAProxy + + Service::HAProxy.const_set :VROUTER_ID, nil + + allow(Service::HAProxy).to receive(:detect_nics).and_return(%w[eth0 eth1 eth2 eth3]) + allow(Service::HAProxy).to receive(:addrs_to_nics).and_return({ + '10.2.11.86' => ['eth0'] + }) + + clear_vars Service::HAProxy + + output = <<~'DYNAMIC' + frontend lb0_5432 + mode tcp + bind 10.2.11.86:5432 + default_backend lb0_5432 + + backend lb0_5432 + mode tcp + balance roundrobin + option tcp-check + server lb0_10.2.11.202_2345 10.2.11.202:2345 check observe layer4 error-limit 50 on-error mark-down + server lb0_10.2.11.203_2345 10.2.11.203:2345 check observe layer4 error-limit 50 on-error mark-down + DYNAMIC + + Dir.mktmpdir do |dir| + haproxy_vars = Service::HAProxy.extract_backends vms + Service::HAProxy.render_servers_cfg haproxy_vars, basedir: dir + result = File.read "#{dir}/servers.cfg" + expect(result.strip).to eq output.strip + end + end +end diff --git a/appliances/VRouter/Keepalived/main.rb b/appliances/VRouter/Keepalived/main.rb new file mode 100644 index 00000000..585624a3 --- /dev/null +++ b/appliances/VRouter/Keepalived/main.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +require 'erb' +require_relative '../vrouter.rb' + +module Service +module Keepalived + extend self + + DEPENDS_ON = %w[] + + VROUTER_KEEPALIVED_PASSWORD = env :VROUTER_KEEPALIVED_PASSWORD, nil + ONEAPP_VNF_KEEPALIVED_PASSWORD = env :ONEAPP_VNF_KEEPALIVED_PASSWORD, VROUTER_KEEPALIVED_PASSWORD # must be under 8 characters + + ONEAPP_VNF_KEEPALIVED_INTERVAL = env :ONEAPP_VNF_KEEPALIVED_INTERVAL, '1' + ONEAPP_VNF_KEEPALIVED_PRIORITY = env :ONEAPP_VNF_KEEPALIVED_PRIORITY, '100' + + VROUTER_KEEPALIVED_ID = env :VROUTER_KEEPALIVED_ID, nil + ONEAPP_VNF_KEEPALIVED_VRID = env :ONEAPP_VNF_KEEPALIVED_VRID, VROUTER_KEEPALIVED_ID + + ONEAPP_VNF_KEEPALIVED_INTERFACES = env :ONEAPP_VNF_KEEPALIVED_INTERFACES, '' # nil -> none, empty -> all + + def parse_env + @interfaces ||= parse_interfaces ONEAPP_VNF_KEEPALIVED_INTERFACES + @mgmt ||= detect_mgmt_interfaces + @nics ||= addrs_to_nics(@interfaces.keys - @mgmt, family: %[inet]).values.flatten.uniq + @vips ||= detect_vips + + (@interfaces.keys - @mgmt).each_with_object({}) do |nic, vars| + vars[:by_nic] ||= {} + vars[:by_nic][nic] = { + password: env("ONEAPP_VNF_KEEPALIVED_#{nic.upcase}_PASSWORD", ONEAPP_VNF_KEEPALIVED_PASSWORD), + interval: env("ONEAPP_VNF_KEEPALIVED_#{nic.upcase}_INTERVAL", ONEAPP_VNF_KEEPALIVED_INTERVAL), + priority: env("ONEAPP_VNF_KEEPALIVED_#{nic.upcase}_PRIORITY", ONEAPP_VNF_KEEPALIVED_PRIORITY), + vrid: env("ONEAPP_VNF_KEEPALIVED_#{nic.upcase}_VRID", ONEAPP_VNF_KEEPALIVED_VRID), + vips: @vips[nic]&.values || [], + noip: !@nics.include?(nic) + } + vars[:by_vrid] ||= {} + vars[:by_vrid][vars[:by_nic][nic][:vrid]] ||= {} + vars[:by_vrid][vars[:by_nic][nic][:vrid]][nic] = vars[:by_nic][nic] + end + end + + def install + msg :info, 'Keepalived::install' + + puts bash 'apk --no-cache add keepalived' + end + + def configure(basedir: '/etc/keepalived') + msg :info, 'Keepalived::configure' + + file "#{basedir}/keepalived.conf", <<~MAIN, mode: 'u=rw,g=r,o=', overwrite: true + include #{basedir}/conf.d/*.conf + MAIN + + file "#{basedir}/conf.d/global.conf", <<~GLOBAL, mode: 'u=rw,g=r,o=', overwrite: true + global_defs { + vrrp_notify_fifo /run/keepalived/vrrp_notify_fifo.sock + fifo_write_vrrp_states_on_reload + } + GLOBAL + + keepalived_vars = parse_env + + file "#{basedir}/conf.d/vrrp.conf", ERB.new(<<~VRRP, trim_mode: '-').result(binding), mode: 'u=rw,g=r,o=', overwrite: true + <%- unless keepalived_vars[:by_vrid].nil? || keepalived_vars[:by_vrid].empty? -%> + vrrp_sync_group VRouter { + group { + <%- keepalived_vars[:by_vrid].each do |_, nics| -%> + <%- unless (kv = nics.find { |_, opt| !opt[:noip] }).nil? -%> + <%= kv[0].upcase %> + <%- end -%> + <%- end -%> + } + } + <%- keepalived_vars[:by_vrid].each do |vrid, nics| -%> + <%- unless (kv = nics.find { |_, opt| !opt[:noip] }).nil? -%> + vrrp_instance <%= kv[0].upcase %> { + state BACKUP + interface <%= kv[0].downcase %> + virtual_router_id <%= vrid %> + priority <%= kv[1][:priority] %> + advert_int <%= kv[1][:interval] %> + virtual_ipaddress { + <%- nics.each do |nic, opt| -%> + <%- opt[:vips].compact.reject(&:empty?).each do |vip| -%> + <%= vip %> dev <%= nic.downcase %> + <%- end -%> + <%- end -%> + } + <%- unless kv[1][:password].nil? -%> + authentication { + auth_type PASS + auth_pass <%= kv[1][:password] %> + } + <%- end -%> + } + <%- end -%> + <%- end -%> + <%- end -%> + VRRP + + # NOTE: It is important to restart keepalived at this point + # to properly re-send vrrp fifo updates to one-failover. + # Re-configure can be triggered by direct context changes + # or for example a NIC hotplug. + toggle [:enable, :restart] + end + + def toggle(operations) + operations.each do |op| + msg :info, "Keepalived::toggle([:#{op}])" + case op + when :enable + puts bash 'rc-update add keepalived default' + when :disable + puts bash 'rc-update del keepalived default ||:' + when :update + puts bash 'rc-update -u' + else + puts bash "rc-service keepalived #{op.to_s}" + end + end + end + + def bootstrap + msg :info, 'Keepalived::bootstrap' + end +end +end diff --git a/appliances/VRouter/Keepalived/tests.rb b/appliances/VRouter/Keepalived/tests.rb new file mode 100644 index 00000000..f0f4f169 --- /dev/null +++ b/appliances/VRouter/Keepalived/tests.rb @@ -0,0 +1,322 @@ +# frozen_string_literal: true + +require 'rspec' +require 'tmpdir' + +def clear_env + ENV.delete_if { |name| name.include?('VROUTER_') || name.include?('_VNF_') } +end + +def clear_vars(object) + object.instance_variables.each { |name| object.remove_instance_variable(name) } +end + +RSpec.describe self do + it 'should provide and parse all env vars' do + clear_env + + ENV['ONEAPP_VNF_KEEPALIVED_INTERVAL'] = '1' + ENV['ONEAPP_VNF_KEEPALIVED_PRIORITY'] = '100' + + ENV['VROUTER_KEEPALIVED_ID'] = '11' + ENV['ONEAPP_VNF_KEEPALIVED_VRID'] = '11' + + ENV['ONEAPP_VNF_KEEPALIVED_INTERFACES'] = 'eth0 eth1' + ENV['ETH8_VROUTER_MANAGEMENT'] = 'YES' + + ENV['ONEAPP_VNF_KEEPALIVED_ETH0_INTERVAL'] = '1' + ENV['ONEAPP_VNF_KEEPALIVED_ETH0_PRIORITY'] = '100' + ENV['ONEAPP_VNF_KEEPALIVED_ETH0_VRID'] = '11' + + ENV['ETH0_VROUTER_IP'] = '10.2.11.69' + ENV['ONEAPP_VROUTER_ETH0_VIP0'] = '10.2.11.69' + ENV['ONEAPP_VROUTER_ETH0_VIP1'] = '10.2.11.86' + + ENV['ONEAPP_VNF_KEEPALIVED_ETH1_INTERVAL'] = '1' + ENV['ONEAPP_VNF_KEEPALIVED_ETH1_PRIORITY'] = '100' + ENV['ONEAPP_VNF_KEEPALIVED_ETH1_VRID'] = '11' + + ENV['ETH1_VROUTER_IP'] = '10.2.12.69' + ENV['ONEAPP_VROUTER_ETH1_VIP0'] = '10.2.12.69' + ENV['ONEAPP_VROUTER_ETH1_VIP1'] = '10.2.12.86' + + load './main.rb'; include Service::Keepalived + + expect(Service::Keepalived::ONEAPP_VNF_KEEPALIVED_INTERVAL).to eq '1' + expect(Service::Keepalived::ONEAPP_VNF_KEEPALIVED_PRIORITY).to eq '100' + expect(Service::Keepalived::VROUTER_KEEPALIVED_ID).to eq '11' + expect(Service::Keepalived::ONEAPP_VNF_KEEPALIVED_VRID).to eq '11' + + allow(Service::Keepalived).to receive(:detect_nics).and_return(%w[eth0 eth1 eth2]) + allow(Service::Keepalived).to receive(:ip_addr_list).and_return([ + { 'ifname' => 'eth0', + 'addr_info' => [] }, + + { 'ifname' => 'eth1', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '172.16.1.1', + 'prefixlen' => 16 } ] } + ]) + + clear_vars Service::Keepalived + + keepalived_vars = Service::Keepalived.parse_env + + expect(Service::Keepalived.instance_variable_get(:@interfaces).keys).to eq %w[eth0 eth1] + expect(Service::Keepalived.instance_variable_get(:@mgmt)).to eq %w[eth8] + + expect(keepalived_vars[:by_nic]['eth0'][:interval]).to eq '1' + expect(keepalived_vars[:by_nic]['eth0'][:priority]).to eq '100' + expect(keepalived_vars[:by_nic]['eth0'][:vrid]).to eq '11' + expect(keepalived_vars[:by_nic]['eth0'][:vips][0]).to eq '10.2.11.69' + expect(keepalived_vars[:by_nic]['eth0'][:vips][1]).to eq '10.2.11.86' + expect(keepalived_vars[:by_nic]['eth0'][:noip]).to be true + + expect(keepalived_vars[:by_nic]['eth1'][:interval]).to eq '1' + expect(keepalived_vars[:by_nic]['eth1'][:priority]).to eq '100' + expect(keepalived_vars[:by_nic]['eth1'][:vrid]).to eq '11' + expect(keepalived_vars[:by_nic]['eth1'][:vips][0]).to eq '10.2.12.69' + expect(keepalived_vars[:by_nic]['eth1'][:vips][1]).to eq '10.2.12.86' + expect(keepalived_vars[:by_nic]['eth1'][:noip]).to be false + + expect(keepalived_vars[:by_vrid]['11'].keys).to eq %w[eth0 eth1] + end + + it 'should get default values from legacy env vars' do + clear_env + + ENV['ONEAPP_VNF_KEEPALIVED_INTERVAL'] = '1' + ENV['ONEAPP_VNF_KEEPALIVED_PRIORITY'] = '100' + + ENV['VROUTER_KEEPALIVED_ID'] = '21' + ENV['ONEAPP_VNF_KEEPALIVED_VRID'] = '' + + ENV['ONEAPP_VNF_KEEPALIVED_INTERFACES'] = 'eth0 eth1' + + ENV['ETH0_VROUTER_IP'] = '10.2.21.69' + ENV['ONEAPP_VROUTER_ETH0_VIP0'] = '' + + ENV['ETH1_VROUTER_IP'] = '10.2.22.69' + ENV['ONEAPP_VROUTER_ETH1_VIP0'] = '' + + load './main.rb'; include Service::Keepalived + + expect(Service::Keepalived::VROUTER_KEEPALIVED_ID).to eq '21' + expect(Service::Keepalived::ONEAPP_VNF_KEEPALIVED_VRID).to eq '21' + + allow(Service::Keepalived).to receive(:detect_nics).and_return(%w[eth0 eth1 eth2]) + allow(Service::Keepalived).to receive(:ip_addr_list).and_return([ + { 'ifname' => 'eth0', + 'addr_info' => [] }, + + { 'ifname' => 'eth1', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '172.16.1.1', + 'prefixlen' => 16 } ] } + ]) + + clear_vars Service::Keepalived + + keepalived_vars = Service::Keepalived.parse_env + + expect(Service::Keepalived.instance_variable_get(:@interfaces).keys).to eq %w[eth0 eth1] + + expect(keepalived_vars[:by_nic]['eth0'][:vrid]).to eq '21' + expect(keepalived_vars[:by_nic]['eth0'][:vips][0]).to eq '10.2.21.69' + expect(keepalived_vars[:by_nic]['eth0'][:noip]).to be true + + expect(keepalived_vars[:by_nic]['eth1'][:vrid]).to eq '21' + expect(keepalived_vars[:by_nic]['eth1'][:vips][0]).to eq '10.2.22.69' + expect(keepalived_vars[:by_nic]['eth1'][:noip]).to be false + + expect(keepalived_vars[:by_vrid]['21'].keys).to eq %w[eth0 eth1] + end + + it 'should render vrrp.conf' do + clear_env + + ENV['ONEAPP_VNF_KEEPALIVED_INTERFACES'] = 'eth0 eth1 eth2 eth3' + + ENV['ONEAPP_VNF_KEEPALIVED_ETH0_INTERVAL'] = '1' + ENV['ONEAPP_VNF_KEEPALIVED_ETH0_PRIORITY'] = '100' + ENV['ONEAPP_VNF_KEEPALIVED_ETH0_VRID'] = '30' + + ENV['ONEAPP_VROUTER_ETH0_VIP0'] = '10.2.30.69' + ENV['ONEAPP_VROUTER_ETH0_VIP1'] = '10.2.30.86' + + ENV['ONEAPP_VNF_KEEPALIVED_ETH1_INTERVAL'] = '1' + ENV['ONEAPP_VNF_KEEPALIVED_ETH1_PRIORITY'] = '100' + ENV['ONEAPP_VNF_KEEPALIVED_ETH1_VRID'] = '30' + + ENV['ONEAPP_VROUTER_ETH1_VIP0'] = '10.2.31.69' + ENV['ONEAPP_VROUTER_ETH1_VIP1'] = '10.2.31.86' + + ENV['ONEAPP_VNF_KEEPALIVED_ETH2_INTERVAL'] = '1' + ENV['ONEAPP_VNF_KEEPALIVED_ETH2_PRIORITY'] = '100' + ENV['ONEAPP_VNF_KEEPALIVED_ETH2_VRID'] = '31' + + ENV['ONEAPP_VROUTER_ETH2_VIP0'] = '10.2.32.69/24' + ENV['ONEAPP_VROUTER_ETH2_VIP1'] = '10.2.32.86/24' + + ENV['ONEAPP_VNF_KEEPALIVED_ETH3_VRID'] = '32' + + load './main.rb'; include Service::Keepalived + + allow(Service::Keepalived).to receive(:detect_nics).and_return(%w[eth0 eth1 eth2 eth3]) + allow(Service::Keepalived).to receive(:ip_addr_list).and_return([ + { 'ifname' => 'eth0', + 'addr_info' => [] }, + + { 'ifname' => 'eth1', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '10.2.31.2', + 'prefixlen' => 24 } ] }, + + { 'ifname' => 'eth2', + 'addr_info' => [] }, + + # { 'ifname' => 'eth2', + # 'addr_info' => [ { 'family' => 'inet', + # 'local' => '10.2.32.2', + # 'prefixlen' => 24 } ] }, + + { 'ifname' => 'eth3', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '10.2.33.2', + 'prefixlen' => 24 } ] } + ]) + allow(Service::Keepalived).to receive(:toggle).and_return(nil) + + clear_vars Service::Keepalived + + output = <<~'VRRP' + vrrp_sync_group VRouter { + group { + ETH1 + ETH3 + } + } + vrrp_instance ETH1 { + state BACKUP + interface eth1 + virtual_router_id 30 + priority 100 + advert_int 1 + virtual_ipaddress { + 10.2.30.69 dev eth0 + 10.2.30.86 dev eth0 + 10.2.31.69 dev eth1 + 10.2.31.86 dev eth1 + } + } + vrrp_instance ETH3 { + state BACKUP + interface eth3 + virtual_router_id 32 + priority 100 + advert_int 1 + virtual_ipaddress { + } + } + VRRP + Dir.mktmpdir do |dir| + Service::Keepalived.configure basedir: dir + result = File.read "#{dir}/conf.d/vrrp.conf" + expect(result.strip).to eq output.strip + end + end + + it 'should render vrrp.conf (passwords)' do + clear_env + + ENV['VROUTER_KEEPALIVED_PASSWORD'] = 'asd123' + ENV['ONEAPP_VNF_KEEPALIVED_ETH3_PASSWORD'] = 'asd456' + + ENV['ONEAPP_VNF_KEEPALIVED_INTERFACES'] = 'eth0 eth1 eth2 eth3' + + ENV['ONEAPP_VNF_KEEPALIVED_ETH0_VRID'] = '30' + ENV['ONEAPP_VNF_KEEPALIVED_ETH1_VRID'] = '30' + ENV['ONEAPP_VNF_KEEPALIVED_ETH2_VRID'] = '31' + ENV['ONEAPP_VNF_KEEPALIVED_ETH3_VRID'] = '32' + + load './main.rb'; include Service::Keepalived + + allow(Service::Keepalived).to receive(:detect_nics).and_return(%w[eth0 eth1 eth2 eth3]) + allow(Service::Keepalived).to receive(:ip_addr_list).and_return([ + { 'ifname' => 'eth0', + 'addr_info' => [] }, + + { 'ifname' => 'eth1', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '10.2.31.2', + 'prefixlen' => 24 } ] }, + + { 'ifname' => 'eth2', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '10.2.32.2', + 'prefixlen' => 24 } ] }, + + { 'ifname' => 'eth3', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '10.2.33.2', + 'prefixlen' => 24 } ] } + ]) + allow(Service::Keepalived).to receive(:toggle).and_return(nil) + + clear_vars Service::Keepalived + + output = <<~'VRRP' + vrrp_sync_group VRouter { + group { + ETH1 + ETH2 + ETH3 + } + } + vrrp_instance ETH1 { + state BACKUP + interface eth1 + virtual_router_id 30 + priority 100 + advert_int 1 + virtual_ipaddress { + } + authentication { + auth_type PASS + auth_pass asd123 + } + } + vrrp_instance ETH2 { + state BACKUP + interface eth2 + virtual_router_id 31 + priority 100 + advert_int 1 + virtual_ipaddress { + } + authentication { + auth_type PASS + auth_pass asd123 + } + } + vrrp_instance ETH3 { + state BACKUP + interface eth3 + virtual_router_id 32 + priority 100 + advert_int 1 + virtual_ipaddress { + } + authentication { + auth_type PASS + auth_pass asd456 + } + } + VRRP + Dir.mktmpdir do |dir| + Service::Keepalived.configure basedir: dir + result = File.read "#{dir}/conf.d/vrrp.conf" + expect(result.strip).to eq output.strip + end + end +end diff --git a/appliances/VRouter/LVS/execute.rb b/appliances/VRouter/LVS/execute.rb new file mode 100644 index 00000000..1409f370 --- /dev/null +++ b/appliances/VRouter/LVS/execute.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'erb' +require_relative '../vrouter.rb' + +module Service +module LVS + extend self + + VROUTER_ID = env :VROUTER_ID, nil + + def extract_backends(objects = {}) + static = backends.from_env(prefix: 'ONEAPP_VNF_LB') + + dynamic = VROUTER_ID.nil? ? backends.from_vms(objects, prefix: 'ONEGATE_LB') + : backends.from_vnets(objects, prefix: 'ONEGATE_LB') + + # NOTE: This ensures that backends can be added dynamically only to statically defined LBs. + merged = hashmap.combine static, backends.intersect(static, dynamic) + + # Replace all "" placeholders where possible. + backends.resolve_vips merged + end + + def render_lvs_conf(lvs_vars, basedir: '/etc/keepalived') + @interfaces ||= parse_interfaces ONEAPP_VNF_LB_INTERFACES + @mgmt ||= detect_mgmt_interfaces + @addrs ||= addrs_to_nics(@interfaces.keys - @mgmt, family: %[inet]).keys + + file "#{basedir}/conf.d/lvs.conf", ERB.new(<<~LVS, trim_mode: '-').result(binding), mode: 'u=rw,g=r,o=', overwrite: true + <%- lvs_vars[:by_endpoint]&.each do |(lb_idx, ip, port), servers| -%> + <%- if @addrs.include?(ip) -%> + virtual_server <%= ip %> <%= port %> { + <%- unless lvs_vars[:options][lb_idx][:scheduler].nil? -%> + lb_algo <%= lvs_vars[:options][lb_idx][:scheduler] %> + <%- end -%> + <%- unless lvs_vars[:options][lb_idx][:method].nil? -%> + lb_kind <%= lvs_vars[:options][lb_idx][:method] %> + <%- end -%> + <%- unless lvs_vars[:options][lb_idx][:protocol].nil? -%> + protocol <%= lvs_vars[:options][lb_idx][:protocol] %> + <%- end -%> + + <%- servers&.values&.each do |s| -%> + real_server <%= s[:host] %> <%= s[:port] %> { + <%- unless s[:weight].nil? -%> + weight <%= s[:weight] %> + <%- end -%> + <%- unless s[:ulimit].nil? -%> + uthreshold <%= s[:ulimit] %> + <%- end -%> + <%- unless s[:llimit].nil? -%> + lthreshold <%= s[:llimit] %> + <%- end -%> + PING_CHECK { + retry 4 + } + } + <%- end -%> + } + <%- end -%> + <%- end -%> + LVS + end + + def execute(basedir: '/etc/keepalived') + msg :info, 'LVS::execute' + + # Handle "static" load-balancers. + render_lvs_conf extract_backends, basedir: basedir + toggle [:reload] + + if ONEAPP_VNF_LB_ONEGATE_ENABLED + prev = [] + + get_objects = VROUTER_ID.nil? ? :get_service_vms : :get_vrouter_vnets + + loop do + unless (objects = method(get_objects).call).empty? + if prev != (this = extract_backends(objects)) + msg :debug, this + + render_lvs_conf this, basedir: basedir + + toggle [:reload] + end + + prev = this + end + + sleep ONEAPP_VNF_LB_REFRESH_RATE.to_i + end + else + sleep + end + end + + def cleanup(basedir: '/etc/keepalived') + msg :info, 'LVS::cleanup' + + file "#{basedir}/conf.d/lvs.conf", '', mode: 'u=rw,g=r,o=', overwrite: true + + toggle [:reload] + end +end +end diff --git a/appliances/VRouter/LVS/main.rb b/appliances/VRouter/LVS/main.rb new file mode 100644 index 00000000..72e76fd5 --- /dev/null +++ b/appliances/VRouter/LVS/main.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require_relative '../vrouter.rb' +require_relative 'execute.rb' + +module Service +module LVS + extend self + + DEPENDS_ON = %w[Service::Failover] + + ONEAPP_VNF_LB_ENABLED = env :ONEAPP_VNF_LB_ENABLED, 'NO' + ONEAPP_VNF_LB_ONEGATE_ENABLED = env :ONEAPP_VNF_LB_ONEGATE_ENABLED, 'NO' + + ONEAPP_VNF_LB_REFRESH_RATE = env :ONEAPP_VNF_LB_REFRESH_RATE, '30' + ONEAPP_VNF_LB_FWMARK_OFFSET = env :ONEAPP_VNF_LB_FWMARK_OFFSET, '10000' + + ONEAPP_VNF_LB_INTERFACES = env :ONEAPP_VNF_LB_INTERFACES, '' # nil -> none, empty -> all + + def install(initdir: '/etc/init.d') + msg :info, 'LVS::install' + + puts bash 'apk --no-cache add ipvsadm ruby' + + file "#{initdir}/one-lvs", <<~SERVICE, mode: 'u=rwx,g=rx,o=' + #!/sbin/openrc-run + + source /run/one-context/one_env + + command="/usr/bin/ruby" + command_args="-r /etc/one-appliance/lib/helpers.rb -r #{__FILE__} -e Service::LVS.execute" + + command_background="yes" + pidfile="/run/$RC_SVCNAME.pid" + + output_log="/var/log/one-appliance/one-lvs.log" + error_log="/var/log/one-appliance/one-lvs.log" + + depend() { + after net keepalived + } + + stop_post() { + $command -r /etc/one-appliance/lib/helpers.rb -r #{__FILE__} -e Service::LVS.cleanup 1>>$output_log 2>>$error_log + } + SERVICE + + toggle [:update] + end + + def configure(basedir: '/etc/keepalived') + msg :info, 'LVS::configure' + + if ONEAPP_VNF_LB_ENABLED + toggle [:enable] + else + toggle [:disable, :reload] + end + end + + def toggle(operations) + operations.each do |op| + msg :debug, "LVS::toggle([:#{op}])" + case op + when :reload + puts bash 'rc-service keepalived reload' + when :enable + puts bash 'rc-update add one-lvs default' + when :disable + puts bash 'rc-update del one-lvs default ||:' + when :update + puts bash 'rc-update -u' + else + puts bash "rc-service one-lvs #{op.to_s}" + end + end + end + + def bootstrap + msg :info, 'LVS::bootstrap' + end +end +end diff --git a/appliances/VRouter/LVS/tests.rb b/appliances/VRouter/LVS/tests.rb new file mode 100644 index 00000000..dec18df7 --- /dev/null +++ b/appliances/VRouter/LVS/tests.rb @@ -0,0 +1,536 @@ +# frozen_string_literal: true + +require 'rspec' +require 'tmpdir' + +def clear_env + ENV.delete_if { |name| name.include?('VROUTER_') || name.include?('_LB') } +end + +def clear_vars(object) + object.instance_variables.each { |name| object.remove_instance_variable(name) } +end + +RSpec.describe self do + it 'should provide defaults (static)' do + clear_env + + ENV['ONEAPP_VNF_LB_ENABLED'] = 'YES' + ENV['ONEAPP_VNF_LB_REFRESH_RATE'] = '' + ENV['ONEAPP_VNF_LB_FWMARK_OFFSET'] = '' + + ENV['ONEAPP_VNF_LB0_IP'] = '10.2.10.69' + ENV['ONEAPP_VNF_LB0_PORT'] = '1234' + ENV['ONEAPP_VNF_LB0_PROTOCOL'] = 'TCP' + + ENV['ONEAPP_VNF_LB0_SERVER0_HOST'] = '10.2.100.10' + ENV['ONEAPP_VNF_LB0_SERVER0_PORT'] = '12345' + + ENV['ONEAPP_VNF_LB0_SERVER1_HOST'] = '10.2.100.20' + ENV['ONEAPP_VNF_LB0_SERVER1_PORT'] = '12345' + + ENV['ONEAPP_VNF_LB1_IP'] = '10.2.20.69' + ENV['ONEAPP_VNF_LB1_PORT'] = '4321' + ENV['ONEAPP_VNF_LB1_PROTOCOL'] = 'TCP' + + ENV['ONEAPP_VNF_LB1_SERVER0_HOST'] = '10.2.200.10' + ENV['ONEAPP_VNF_LB1_SERVER0_PORT'] = '54321' + + ENV['ONEAPP_VNF_LB1_SERVER1_HOST'] = '10.2.200.20' + ENV['ONEAPP_VNF_LB1_SERVER1_PORT'] = '54321' + + load './main.rb'; include Service::LVS + + expect(Service::LVS::ONEAPP_VNF_LB_ENABLED).to be true + expect(Service::LVS::ONEAPP_VNF_LB_REFRESH_RATE).to eq '30' + expect(Service::LVS::ONEAPP_VNF_LB_FWMARK_OFFSET).to eq '10000' + + Service::LVS.const_set :VROUTER_ID, '86' + + allow(Service::LVS).to receive(:detect_nics).and_return(%w[eth0 eth1 eth2 eth3]) + + expect(Service::LVS.extract_backends).to eq({ + by_endpoint: { + [ 0, '10.2.10.69', '1234' ] => + { [ '10.2.100.10', '12345' ] => { host: '10.2.100.10', port: '12345' }, + [ '10.2.100.20', '12345' ] => { host: '10.2.100.20', port: '12345' } }, + + [ 1, '10.2.20.69', '4321' ] => + { [ '10.2.200.10', '54321' ] => { host: '10.2.200.10', port: '54321' }, + [ '10.2.200.20', '54321' ] => { host: '10.2.200.20', port: '54321' } } }, + + options: { 0 => { ip: '10.2.10.69', port: '1234', protocol: 'TCP' }, + 1 => { ip: '10.2.20.69', port: '4321', protocol: 'TCP' } } + }) + end + + it 'should provide and parse all env vars (static)' do + clear_env + + ENV['ONEAPP_VNF_LB_ENABLED'] = 'YES' + ENV['ONEAPP_VNF_LB_REFRESH_RATE'] = '45' + ENV['ONEAPP_VNF_LB_FWMARK_OFFSET'] = '12345' + + ENV['ONEAPP_VNF_LB0_IP'] = '10.2.10.69' + ENV['ONEAPP_VNF_LB0_PORT'] = '1234' + ENV['ONEAPP_VNF_LB0_PROTOCOL'] = 'TCP' + ENV['ONEAPP_VNF_LB0_METHOD'] = 'DR' + ENV['ONEAPP_VNF_LB0_TIMEOUT'] = '10' + ENV['ONEAPP_VNF_LB0_SCHEDULER'] = 'rr' + + ENV['ONEAPP_VNF_LB0_SERVER0_HOST'] = '10.2.100.10' + ENV['ONEAPP_VNF_LB0_SERVER0_PORT'] = '12345' + ENV['ONEAPP_VNF_LB0_SERVER0_WEIGHT'] = '1' + ENV['ONEAPP_VNF_LB0_SERVER0_ULIMIT'] = '100' + ENV['ONEAPP_VNF_LB0_SERVER0_LLIMIT'] = '0' + + ENV['ONEAPP_VNF_LB0_SERVER1_HOST'] = '10.2.100.20' + ENV['ONEAPP_VNF_LB0_SERVER1_PORT'] = '12345' + ENV['ONEAPP_VNF_LB0_SERVER1_WEIGHT'] = '1' + ENV['ONEAPP_VNF_LB0_SERVER1_ULIMIT'] = '100' + ENV['ONEAPP_VNF_LB0_SERVER1_LLIMIT'] = '0' + + ENV['ONEAPP_VNF_LB1_IP'] = '10.2.20.69' + ENV['ONEAPP_VNF_LB1_PORT'] = '4321' + ENV['ONEAPP_VNF_LB1_PROTOCOL'] = 'TCP' + ENV['ONEAPP_VNF_LB1_METHOD'] = 'DR' + ENV['ONEAPP_VNF_LB1_SCHEDULER'] = 'rr' + + ENV['ONEAPP_VNF_LB1_SERVER0_HOST'] = '10.2.200.10' + ENV['ONEAPP_VNF_LB1_SERVER0_PORT'] = '54321' + ENV['ONEAPP_VNF_LB1_SERVER0_WEIGHT'] = '1' + ENV['ONEAPP_VNF_LB1_SERVER0_ULIMIT'] = '100' + ENV['ONEAPP_VNF_LB1_SERVER0_LLIMIT'] = '0' + + ENV['ONEAPP_VNF_LB1_SERVER1_HOST'] = '10.2.200.20' + ENV['ONEAPP_VNF_LB1_SERVER1_PORT'] = '54321' + ENV['ONEAPP_VNF_LB1_SERVER1_WEIGHT'] = '1' + ENV['ONEAPP_VNF_LB1_SERVER1_ULIMIT'] = '100' + ENV['ONEAPP_VNF_LB1_SERVER1_LLIMIT'] = '0' + + load './main.rb'; include Service::LVS + + expect(Service::LVS::ONEAPP_VNF_LB_ENABLED).to be true + expect(Service::LVS::ONEAPP_VNF_LB_REFRESH_RATE).to eq '45' + expect(Service::LVS::ONEAPP_VNF_LB_FWMARK_OFFSET).to eq '12345' + + Service::LVS.const_set :VROUTER_ID, '86' + + allow(Service::LVS).to receive(:detect_nics).and_return(%w[eth0 eth1 eth2 eth3]) + + expect(Service::LVS.extract_backends).to eq({ + by_endpoint: { + [ 0, '10.2.10.69', '1234' ] => + { [ '10.2.100.10', '12345' ] => { host: '10.2.100.10', port: '12345', llimit: '0', ulimit: '100', weight: '1' }, + [ '10.2.100.20', '12345' ] => { host: '10.2.100.20', port: '12345', llimit: '0', ulimit: '100', weight: '1' } }, + + [ 1, '10.2.20.69', '4321' ] => + { [ '10.2.200.10', '54321' ] => { host: '10.2.200.10', port: '54321', llimit: '0', ulimit: '100', weight: '1' }, + [ '10.2.200.20', '54321' ] => { host: '10.2.200.20', port: '54321', llimit: '0', ulimit: '100', weight: '1' } } }, + + options: { 0 => { ip: '10.2.10.69', port: '1234', method: 'DR', protocol: 'TCP', scheduler: 'rr' }, + 1 => { ip: '10.2.20.69', port: '4321', method: 'DR', protocol: 'TCP', scheduler: 'rr' } } + + }) + end + + it 'should render lvs.cfg (static)' do + clear_env + + ENV['ONEAPP_VNF_LB_ENABLED'] = 'YES' + ENV['ONEAPP_VNF_LB_REFRESH_RATE'] = '' + ENV['ONEAPP_VNF_LB_FWMARK_OFFSET'] = '' + + ENV['ONEAPP_VNF_LB0_IP'] = '10.2.10.69' + ENV['ONEAPP_VNF_LB0_PORT'] = '1234' + ENV['ONEAPP_VNF_LB0_PROTOCOL'] = 'TCP' + ENV['ONEAPP_VNF_LB0_METHOD'] = 'DR' + ENV['ONEAPP_VNF_LB0_TIMEOUT'] = '10' + ENV['ONEAPP_VNF_LB0_SCHEDULER'] = 'rr' + + ENV['ONEAPP_VNF_LB0_SERVER0_HOST'] = '10.2.100.10' + ENV['ONEAPP_VNF_LB0_SERVER0_PORT'] = '12345' + + ENV['ONEAPP_VNF_LB0_SERVER1_HOST'] = '10.2.100.20' + ENV['ONEAPP_VNF_LB0_SERVER1_PORT'] = '12345' + + ENV['ONEAPP_VNF_LB1_IP'] = '10.2.20.69' + ENV['ONEAPP_VNF_LB1_PORT'] = '4321' + ENV['ONEAPP_VNF_LB1_PROTOCOL'] = 'TCP' + ENV['ONEAPP_VNF_LB1_METHOD'] = 'DR' + ENV['ONEAPP_VNF_LB1_TIMEOUT'] = '10' + ENV['ONEAPP_VNF_LB1_SCHEDULER'] = 'rr' + + ENV['ONEAPP_VNF_LB1_SERVER0_HOST'] = '10.2.200.10' + ENV['ONEAPP_VNF_LB1_SERVER0_PORT'] = '54321' + + ENV['ONEAPP_VNF_LB1_SERVER1_HOST'] = '10.2.200.20' + ENV['ONEAPP_VNF_LB1_SERVER1_PORT'] = '54321' + + load './main.rb'; include Service::LVS + + Service::LVS.const_set :VROUTER_ID, '86' + + allow(Service::LVS).to receive(:toggle).and_return(nil) + allow(Service::LVS).to receive(:sleep).and_return(nil) + allow(Service::LVS).to receive(:detect_nics).and_return(%w[eth0 eth1 eth2 eth3]) + allow(Service::LVS).to receive(:addrs_to_nics).and_return({ + '10.2.10.69' => ['eth0'], + '10.2.20.69' => ['eth0'] + }) + + clear_vars Service::LVS + + output = <<~STATIC + virtual_server 10.2.10.69 1234 { + lb_algo rr + lb_kind DR + protocol TCP + + real_server 10.2.100.10 12345 { + PING_CHECK { + retry 4 + } + } + real_server 10.2.100.20 12345 { + PING_CHECK { + retry 4 + } + } + } + virtual_server 10.2.20.69 4321 { + lb_algo rr + lb_kind DR + protocol TCP + + real_server 10.2.200.10 54321 { + PING_CHECK { + retry 4 + } + } + real_server 10.2.200.20 54321 { + PING_CHECK { + retry 4 + } + } + } + STATIC + + Dir.mktmpdir do |dir| + Service::LVS.execute basedir: dir + result = File.read "#{dir}/conf.d/lvs.conf" + expect(result.strip).to eq output.strip + end + end + + it 'should render lvs.cfg (dynamic)' do + clear_env + + ENV['ONEAPP_VNF_LB_ENABLED'] = 'YES' + ENV['ONEAPP_VNF_LB_ONEGATE_ENABLED'] = 'YES' + + ENV['ONEAPP_VNF_LB_FWMARK_OFFSET'] = '' + + ENV['ONEAPP_VNF_LB0_IP'] = '10.2.11.86' + ENV['ONEAPP_VNF_LB0_PORT'] = '6969' + ENV['ONEAPP_VNF_LB0_PROTOCOL'] = 'TCP' + ENV['ONEAPP_VNF_LB0_METHOD'] = 'DR' + ENV['ONEAPP_VNF_LB0_TIMEOUT'] = '5' + ENV['ONEAPP_VNF_LB0_SCHEDULER'] = 'rr' + + ENV['ONEAPP_VNF_LB0_SERVER0_HOST'] = '10.2.11.200' + ENV['ONEAPP_VNF_LB0_SERVER0_PORT'] = '6969' + + ENV['ONEAPP_VNF_LB0_SERVER1_HOST'] = '10.2.11.201' + ENV['ONEAPP_VNF_LB0_SERVER1_PORT'] = '6969' + + ENV['ONEAPP_VNF_LB1_IP'] = '10.2.11.86' + ENV['ONEAPP_VNF_LB1_PORT'] = '8686' + ENV['ONEAPP_VNF_LB1_PROTOCOL'] = 'TCP' + ENV['ONEAPP_VNF_LB1_METHOD'] = 'DR' + ENV['ONEAPP_VNF_LB1_TIMEOUT'] = '5' + ENV['ONEAPP_VNF_LB1_SCHEDULER'] = 'rr' + + (vnets ||= []) << JSON.parse(<<~'VNET0') + { + "VNET": { + "ID": "0", + "AR_POOL": { + "AR": [ + { + "AR_ID": "0", + "LEASES": { + "LEASE": [ + { + "IP": "10.2.11.202", + "MAC": "02:00:0a:02:0b:ca", + "VM": "167", + "NIC_NAME": "NIC0", + "BACKEND": "YES", + + "ONEGATE_LB0_IP": "10.2.11.86", + "ONEGATE_LB0_PORT": "6969", + "ONEGATE_LB0_PROTOCOL": "TCP", + + "ONEGATE_LB0_SERVER_HOST": "10.2.11.202", + "ONEGATE_LB0_SERVER_PORT": "6969", + "ONEGATE_LB0_SERVER_WEIGHT": "3" + }, + { + "IP": "10.2.11.201", + "MAC": "02:00:0a:02:0b:c9", + "VM": "167", + "NIC_NAME": "NIC0", + "BACKEND": "YES", + + "ONEGATE_LB0_IP": "10.2.11.86", + "ONEGATE_LB0_PORT": "6969", + "ONEGATE_LB0_PROTOCOL": "TCP", + + "ONEGATE_LB0_SERVER_HOST": "10.2.11.201", + "ONEGATE_LB0_SERVER_PORT": "6969", + "ONEGATE_LB0_SERVER_WEIGHT": "2", + + "ONEGATE_LB1_IP": "10.2.11.86", + "ONEGATE_LB1_PORT": "8686", + + "ONEGATE_LB1_SERVER_HOST": "10.2.11.201", + "ONEGATE_LB1_SERVER_PORT": "8686", + "ONEGATE_LB1_SERVER_WEIGHT": "2" + }, + { + "IP": "10.2.11.200", + "MAC": "02:00:0a:02:0b:c8", + "VM": "167", + "NIC_NAME": "NIC0", + "BACKEND": "YES", + + "ONEGATE_LB0_IP": "10.2.11.86", + "ONEGATE_LB0_PORT": "6969", + + "ONEGATE_LB0_SERVER_HOST": "10.2.11.200", + "ONEGATE_LB0_SERVER_PORT": "6969", + "ONEGATE_LB0_SERVER_WEIGHT": "1", + + "ONEGATE_LB1_IP": "10.2.11.86", + "ONEGATE_LB1_PORT": "8686", + + "ONEGATE_LB1_SERVER_HOST": "10.2.11.200", + "ONEGATE_LB1_SERVER_PORT": "8686", + "ONEGATE_LB1_SERVER_WEIGHT": "1" + } + ] + } + } + ] + } + } + } + VNET0 + + load './main.rb'; include Service::LVS + + Service::LVS.const_set :VROUTER_ID, '86' + + allow(Service::LVS).to receive(:detect_nics).and_return(%w[eth0 eth1 eth2 eth3]) + allow(Service::LVS).to receive(:addrs_to_nics).and_return({ + '10.2.11.86' => ['eth0'] + }) + + clear_vars Service::LVS + + output = <<~'DYNAMIC' + virtual_server 10.2.11.86 6969 { + lb_algo rr + lb_kind DR + protocol TCP + + real_server 10.2.11.200 6969 { + weight 1 + PING_CHECK { + retry 4 + } + } + real_server 10.2.11.201 6969 { + weight 2 + PING_CHECK { + retry 4 + } + } + real_server 10.2.11.202 6969 { + weight 3 + PING_CHECK { + retry 4 + } + } + } + virtual_server 10.2.11.86 8686 { + lb_algo rr + lb_kind DR + protocol TCP + + real_server 10.2.11.201 8686 { + weight 2 + PING_CHECK { + retry 4 + } + } + real_server 10.2.11.200 8686 { + weight 1 + PING_CHECK { + retry 4 + } + } + } + DYNAMIC + + Dir.mktmpdir do |dir| + lvs_vars = Service::LVS.extract_backends vnets + Service::LVS.render_lvs_conf lvs_vars, basedir: dir + result = File.read "#{dir}/conf.d/lvs.conf" + expect(result.strip).to eq output.strip + end + end + + it 'should render lvs.cfg (dynamic/OneFlow)' do + clear_env + + ENV['ONEAPP_VNF_LB_ENABLED'] = 'YES' + ENV['ONEAPP_VNF_LB_ONEGATE_ENABLED'] = 'YES' + + ENV['ONEAPP_VNF_LB_REFRESH_RATE'] = '' + + ENV['ONEAPP_VNF_LB0_IP'] = '10.2.11.86' + ENV['ONEAPP_VNF_LB0_PORT'] = '5432' + ENV['ONEAPP_VNF_LB0_PROTOCOL'] = 'TCP' + ENV['ONEAPP_VNF_LB0_METHOD'] = 'DR' + ENV['ONEAPP_VNF_LB0_TIMEOUT'] = '5' + ENV['ONEAPP_VNF_LB0_SCHEDULER'] = 'rr' + + (vms ||= []) << JSON.parse(<<~'VM0') + { + "VM": { + "NAME": "server_0_(service_23)", + "ID": "435", + "STATE": "3", + "LCM_STATE": "3", + "USER_TEMPLATE": { + "HOT_RESIZE": { + "CPU_HOT_ADD_ENABLED": "NO", + "MEMORY_HOT_ADD_ENABLED": "NO" + }, + "LOGO": "images/logos/linux.png", + "LXD_SECURITY_PRIVILEGED": "true", + "MEMORY_UNIT_COST": "MB", + "ONEGATE_LB0_IP": "10.2.11.86", + "ONEGATE_LB0_PORT": "5432", + "ONEGATE_LB0_SERVER_HOST": "10.2.11.202", + "ONEGATE_LB0_SERVER_PORT": "2345", + "ONEGATE_LB0_SERVER_WEIGHT": "1", + "ROLE_NAME": "server", + "SERVICE_ID": "23" + }, + "TEMPLATE": { + "NIC": [ + { + "IP": "10.2.11.202", + "MAC": "02:00:0a:02:0b:ca", + "NAME": "_NIC0", + "NETWORK": "service" + }, + { + "IP": "172.20.0.122", + "MAC": "02:00:ac:14:00:7a", + "NAME": "_NIC1", + "NETWORK": "private" + } + ], + "NIC_ALIAS": [] + } + } + } + VM0 + (vms ||= []) << JSON.parse(<<~'VM1') + { + "VM": { + "NAME": "server_1_(service_23)", + "ID": "436", + "STATE": "3", + "LCM_STATE": "3", + "USER_TEMPLATE": { + "HOT_RESIZE": { + "CPU_HOT_ADD_ENABLED": "NO", + "MEMORY_HOT_ADD_ENABLED": "NO" + }, + "LOGO": "images/logos/linux.png", + "LXD_SECURITY_PRIVILEGED": "true", + "MEMORY_UNIT_COST": "MB", + "ONEGATE_LB0_IP": "10.2.11.86", + "ONEGATE_LB0_PORT": "5432", + "ONEGATE_LB0_SERVER_HOST": "10.2.11.203", + "ONEGATE_LB0_SERVER_PORT": "2345", + "ONEGATE_LB0_SERVER_WEIGHT": "2", + "ROLE_NAME": "server", + "SERVICE_ID": "23" + }, + "TEMPLATE": { + "NIC": [ + { + "IP": "10.2.11.203", + "MAC": "02:00:0a:02:0b:cb", + "NAME": "_NIC0", + "NETWORK": "service" + }, + { + "IP": "172.20.0.123", + "MAC": "02:00:ac:14:00:7b", + "NAME": "_NIC1", + "NETWORK": "private" + } + ], + "NIC_ALIAS": [] + } + } + } + VM1 + + load './main.rb'; include Service::LVS + + Service::LVS.const_set :VROUTER_ID, nil + + allow(Service::LVS).to receive(:detect_nics).and_return(%w[eth0 eth1 eth2 eth3]) + allow(Service::LVS).to receive(:addrs_to_nics).and_return({ + '10.2.11.86' => ['eth0'] + }) + + clear_vars Service::LVS + + output = <<~'DYNAMIC' + virtual_server 10.2.11.86 5432 { + lb_algo rr + lb_kind DR + protocol TCP + + real_server 10.2.11.202 2345 { + weight 1 + PING_CHECK { + retry 4 + } + } + real_server 10.2.11.203 2345 { + weight 2 + PING_CHECK { + retry 4 + } + } + } + DYNAMIC + + Dir.mktmpdir do |dir| + lvs_vars = Service::LVS.extract_backends vms + Service::LVS.render_lvs_conf lvs_vars, basedir: dir + result = File.read "#{dir}/conf.d/lvs.conf" + expect(result.strip).to eq output.strip + end + end +end diff --git a/appliances/VRouter/NAT4/main.rb b/appliances/VRouter/NAT4/main.rb new file mode 100644 index 00000000..7b71fa5e --- /dev/null +++ b/appliances/VRouter/NAT4/main.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require 'erb' +require_relative '../vrouter.rb' + +module Service +module NAT4 + extend self + + DEPENDS_ON = %w[Service::Failover] + + ONEAPP_VNF_NAT4_ENABLED = env :ONEAPP_VNF_NAT4_ENABLED, 'NO' + + ONEAPP_VNF_NAT4_INTERFACES_OUT = env :ONEAPP_VNF_NAT4_INTERFACES_OUT, '' # nil -> none, empty -> all + + def install(initdir: '/etc/init.d') + msg :info, 'NAT4::install' + + puts bash 'apk --no-cache add iptables-openrc ruby' + + file "#{initdir}/one-nat4", <<~SERVICE, mode: 'u=rwx,g=rx,o=' + #!/sbin/openrc-run + + source /run/one-context/one_env + + command="/usr/bin/ruby" + command_args="-r /etc/one-appliance/lib/helpers.rb -r #{__FILE__}" + + depend() { + after net firewall keepalived + } + + start() { + $command $command_args -e Service::NAT4.execute 1>>/var/log/one-appliance/one-nat4.log 2>&1 + } + + stop() { + $command $command_args -e Service::NAT4.cleanup 1>>/var/log/one-appliance/one-nat4.log 2>&1 + } + SERVICE + + toggle [:update] + end + + def configure + msg :info, 'NAT4::configure' + + if ONEAPP_VNF_NAT4_ENABLED + toggle [:save, :enable] + else + toggle [:stop, :disable] + end + end + + def execute + msg :info, 'NAT4::execute' + + # Add dedicated NAT4 chain. + bash <<~IPTABLES + iptables -t nat -nL NAT4 || iptables -t nat -N NAT4 + iptables -t nat -C POSTROUTING -j NAT4 || iptables -t nat -A POSTROUTING -j NAT4 + IPTABLES + + interfaces_out = parse_interfaces ONEAPP_VNF_NAT4_INTERFACES_OUT + mgmt = detect_mgmt_interfaces + interfaces = interfaces_out.keys - mgmt + + unless interfaces.empty? + # Add NAT4 rules. + bash ERB.new(<<~IPTABLES, trim_mode: '-').result(binding) + iptables -t nat -F NAT4 + <%- interfaces.each do |nic| -%> + iptables -t nat -A NAT4 -o '<%= nic %>' -j MASQUERADE + <%- end -%> + IPTABLES + end + + toggle [:save, :reload] + end + + def cleanup + msg :info, 'NAT4::cleanup' + + # Clear dedicated NAT4 chain. + bash 'iptables -t nat -F NAT4' + + toggle [:save, :reload] + end + + def toggle(operations) + operations.each do |op| + msg :info, "NAT4::toggle([:#{op}])" + case op + when :save + puts bash 'rc-service iptables save' + when :reload + puts bash 'rc-service iptables reload' + when :enable + puts bash 'rc-update add iptables default' + puts bash 'rc-update add one-nat4 default' + when :disable + puts bash 'rc-update del one-nat4 default ||:' + when :update + puts bash 'rc-update -u' + when :start + puts bash 'rc-service iptables start' + puts bash 'rc-service one-nat4 start' + else + puts bash "rc-service one-nat4 #{op.to_s}" + end + end + end + + def bootstrap + msg :info, 'NAT4::bootstrap' + end +end +end diff --git a/appliances/VRouter/Router4/main.rb b/appliances/VRouter/Router4/main.rb new file mode 100644 index 00000000..83d04ca0 --- /dev/null +++ b/appliances/VRouter/Router4/main.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require 'erb' +require_relative '../vrouter.rb' + +module Service +module Router4 + extend self + + DEPENDS_ON = %w[Service::Failover] + + VROUTER_ID = env :VROUTER_ID, nil + + ONEAPP_VNF_ROUTER4_ENABLED = env :ONEAPP_VNF_ROUTER4_ENABLED, (VROUTER_ID.nil? ? 'NO' : 'YES') + + ONEAPP_VNF_ROUTER4_INTERFACES = env :ONEAPP_VNF_ROUTER4_INTERFACES, '' # nil -> none, empty -> all + + def install(initdir: '/etc/init.d') + msg :info, 'Router4::install' + + puts bash 'apk --no-cache add procps ruby' + + file "#{initdir}/one-router4", <<~SERVICE, mode: 'u=rwx,g=rx,o=' + #!/sbin/openrc-run + + source /run/one-context/one_env + + command="/usr/bin/ruby" + command_args="-r /etc/one-appliance/lib/helpers.rb -r #{__FILE__}" + + depend() { + after sysctl net firewall keepalived + } + + start() { + $command $command_args -e Service::Router4.execute 1>>/var/log/one-appliance/one-router4.log 2>&1 + } + + stop() { + $command $command_args -e Service::Router4.cleanup 1>>/var/log/one-appliance/one-router4.log 2>&1 + } + SERVICE + + toggle [:update] + end + + def configure + msg :info, 'Router4::configure' + + if ONEAPP_VNF_ROUTER4_ENABLED + toggle [:enable] + else + toggle [:stop, :disable] + end + end + + def execute(basedir: '/etc/sysctl.d') + msg :info, 'Router4::execute' + + interfaces = parse_interfaces ONEAPP_VNF_ROUTER4_INTERFACES + mgmt = detect_mgmt_interfaces + + to_enable = interfaces.keys - mgmt + to_disable = detect_nics - to_enable + + file "#{basedir}/98-Router4.conf", ERB.new(<<~SYSCTL, trim_mode: '-').result(binding), mode: 'u=rw,go=r', overwrite: true + net.ipv4.ip_forward = 0 + net.ipv4.conf.all.forwarding = 0 + net.ipv4.conf.default.forwarding = 0 + <%- to_enable.each do |nic| -%> + net.ipv4.conf.<%= nic %>.forwarding = 1 + <%- end -%> + <%- to_disable.each do |nic| -%> + net.ipv4.conf.<%= nic %>.forwarding = 0 + <%- end -%> + SYSCTL + + toggle [:reload] + end + + def cleanup(basedir: '/etc/sysctl.d') + msg :info, 'Router4::cleanup' + + to_disable = detect_nics + + file "#{basedir}/98-Router4.conf", ERB.new(<<~SYSCTL, trim_mode: '-').result(binding), mode: 'u=rw,go=r', overwrite: true + net.ipv4.ip_forward = 0 + net.ipv4.conf.all.forwarding = 0 + net.ipv4.conf.default.forwarding = 0 + <%- to_disable.each do |nic| -%> + net.ipv4.conf.<%= nic %>.forwarding = 0 + <%- end -%> + SYSCTL + + toggle [:reload] + end + + def toggle(operations) + operations.each do |op| + msg :info, "Router4::toggle([:#{op}])" + case op + when :enable + puts bash 'rc-update add one-router4 default' + when :disable + puts bash 'rc-update del one-router4 default ||:' + when :update + puts bash 'rc-update -u' + when :reload + puts bash 'sysctl --system' + else + puts bash "rc-service one-router4 #{op.to_s}" + end + end + end + + def bootstrap + msg :info, 'Router4::bootstrap' + end +end +end diff --git a/appliances/VRouter/Router4/tests.rb b/appliances/VRouter/Router4/tests.rb new file mode 100644 index 00000000..da5ed670 --- /dev/null +++ b/appliances/VRouter/Router4/tests.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'rspec' +require 'tmpdir' + +def clear_env + ENV.delete_if { |name| name.include?('_ROUTER4_') } +end + +RSpec.describe self do + it 'should enable forwarding (legacy)' do + clear_env + + ENV['VROUTER_ID'] = '86' + ENV['ONEAPP_VNF_ROUTER4_INTERFACES'] = 'eth0 eth1 eth2' + + load './main.rb'; include Service::Router4 + + allow(Service::Router4).to receive(:detect_nics).and_return(%w[eth0 eth1 eth2 eth3]) + allow(Service::Router4).to receive(:toggle).and_return(nil) + + output = <<~'SYSCTL' + net.ipv4.ip_forward = 0 + net.ipv4.conf.all.forwarding = 0 + net.ipv4.conf.default.forwarding = 0 + net.ipv4.conf.eth0.forwarding = 1 + net.ipv4.conf.eth1.forwarding = 1 + net.ipv4.conf.eth2.forwarding = 1 + net.ipv4.conf.eth3.forwarding = 0 + SYSCTL + + Dir.mktmpdir do |dir| + Service::Router4.execute basedir: dir + result = File.read "#{dir}/98-Router4.conf" + expect(result.strip).to eq output.strip + end + end + + it 'should enable forwarding' do + clear_env + + ENV['ONEAPP_VNF_ROUTER4_ENABLED'] = 'YES' + ENV['ONEAPP_VNF_ROUTER4_INTERFACES'] = 'eth0 eth1' + + load './main.rb'; include Service::Router4 + + allow(Service::Router4).to receive(:detect_nics).and_return(%w[eth0 eth1 eth2 eth3]) + allow(Service::Router4).to receive(:toggle).and_return(nil) + + output = <<~'SYSCTL' + net.ipv4.ip_forward = 0 + net.ipv4.conf.all.forwarding = 0 + net.ipv4.conf.default.forwarding = 0 + net.ipv4.conf.eth0.forwarding = 1 + net.ipv4.conf.eth1.forwarding = 1 + net.ipv4.conf.eth2.forwarding = 0 + net.ipv4.conf.eth3.forwarding = 0 + SYSCTL + + Dir.mktmpdir do |dir| + Service::Router4.execute basedir: dir + result = File.read "#{dir}/98-Router4.conf" + expect(result.strip).to eq output.strip + end + end + + it 'should disable forwarding' do + clear_env + + ENV['ONEAPP_VNF_ROUTER4_ENABLED'] = 'YES' + ENV['ONEAPP_VNF_ROUTER4_INTERFACES'] = 'eth0 eth1' + + load './main.rb'; include Service::Router4 + + allow(Service::Router4).to receive(:detect_nics).and_return(%w[eth0 eth1 eth2 eth3]) + allow(Service::Router4).to receive(:toggle).and_return(nil) + + output = <<~'SYSCTL' + net.ipv4.ip_forward = 0 + net.ipv4.conf.all.forwarding = 0 + net.ipv4.conf.default.forwarding = 0 + net.ipv4.conf.eth0.forwarding = 0 + net.ipv4.conf.eth1.forwarding = 0 + net.ipv4.conf.eth2.forwarding = 0 + net.ipv4.conf.eth3.forwarding = 0 + SYSCTL + + Dir.mktmpdir do |dir| + Service::Router4.cleanup basedir: dir + result = File.read "#{dir}/98-Router4.conf" + expect(result.strip).to eq output.strip + end + end +end diff --git a/appliances/VRouter/SDNAT4/execute.rb b/appliances/VRouter/SDNAT4/execute.rb new file mode 100644 index 00000000..fa22e857 --- /dev/null +++ b/appliances/VRouter/SDNAT4/execute.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +require 'erb' +require 'ipaddr' +require 'json' +require_relative '../vrouter.rb' + +module Service +module SDNAT4 + extend self + + def extract_external(vnets = {}) + @interfaces ||= parse_interfaces ONEAPP_VNF_SDNAT4_INTERFACES + @mgmt ||= detect_mgmt_interfaces + @subnets ||= addrs_to_subnets(@interfaces.keys - @mgmt, family: %w[inet]).values.uniq.map { |s| IPAddr.new(s) } + + vm_map = {} + external = [] + vnets.each do |vn| + next if (vn_id = vn.dig('VNET', 'ID')).nil? + + [vn.dig('VNET', 'AR_POOL', 'AR')].flatten.each do |ar| + ar.dig('LEASES', 'LEASE')&.each do |lease| + vm_map[[lease['VM'], vn_id]] = lease + external << lease if lease['EXTERNAL'] + end + end + end + + ip_map = {} + external.each do |lease| + k = [lease['VM'], lease['PARENT_NETWORK_ID']] + v = vm_map.dig(k, 'IP') + + next if v.nil? + + next unless @subnets.map { |s| s.include?(v) } .any? + + ip_map[lease['IP']] = v + end + + ip_added = [] + document = ip_addr_show 'lo' + document&.dig('addr_info')&.each do |a| + next if a['label'].nil? || a['label'] != 'SDNAT4' + next if a['local'].nil? + + ip_added << a['local'] + end + + to_del = [] + ip_added.each do |ext| + to_del << ext unless ip_map.keys.include?(ext) + end + + to_add = [] + ip_map.each do |ext, _| + to_add << ext unless ip_added.include?(ext) + end + + { external: external, ip_map: ip_map, to_del: to_del, to_add: to_add } + end + + def apply(sdnat4_vars) + # Add SDNAT4 rules. + bash ERB.new(<<~IPTABLES, trim_mode: '-').result(binding) + iptables -t nat -F SNAT4 + iptables -t nat -F DNAT4 + <%- sdnat4_vars[:ip_map].each do |ext, int| -%> + iptables -t nat -A SNAT4 -s '<%= int %>/32' -j SNAT --to-s '<%= ext %>' + iptables -t nat -A DNAT4 -d '<%= ext %>/32' -j DNAT --to-d '<%= int %>' + <%- end -%> + IPTABLES + + # Delete / Add IP aliases. + bash ERB.new(<<~IP, trim_mode: '-').result(binding) + <%- sdnat4_vars[:to_del].each do |ext| -%> + ip address del '<%= ext %>/32' dev lo label SDNAT4 + <%- end -%> + <%- sdnat4_vars[:to_add].each do |ext| -%> + ip address add '<%= ext %>/32' dev lo label SDNAT4 + <%- end -%> + IP + end + + def execute + msg :info, 'SDNAT4::execute' + + prev = [] + + if ONEAPP_VNF_SDNAT4_ENABLED + # Add dedicated SNAT4 chain. + bash <<~IPTABLES + iptables -t nat -nL SNAT4 || iptables -t nat -N SNAT4 + iptables -t nat -C POSTROUTING -j SNAT4 || iptables -t nat -I POSTROUTING 1 -j SNAT4 + IPTABLES + + # Add dedicated DNAT4 chain. + bash <<~IPTABLES + iptables -t nat -nL DNAT4 || iptables -t nat -N DNAT4 + iptables -t nat -C PREROUTING -j DNAT4 || iptables -t nat -I PREROUTING 1 -j DNAT4 + IPTABLES + + toggle [:save] + + loop do + unless (vnets = get_vrouter_vnets).empty? + if prev != (this = extract_external(vnets))[:external] + msg :debug, this + + apply this + + toggle [:save, :reload] + end + + prev = this[:external] + end + + sleep ONEAPP_VNF_SDNAT4_REFRESH_RATE.to_i + end + else + sleep + end + end + + def cleanup + msg :info, 'SDNAT4::cleanup' + + # Clear dedicated SDNAT4 chains. + bash <<~IPTABLES + if iptables -t nat -nL SNAT4; then iptables -t nat -F SNAT4; fi + if iptables -t nat -nL DNAT4; then iptables -t nat -F DNAT4; fi + IPTABLES + + # Clear all SDNAT4-labeled IPs. + document = ip_addr_show 'lo' + document&.dig('addr_info')&.each do |a| + next if a['label'].nil? || a['label'] != 'SDNAT4' + next if a['local'].nil? + + bash "ip address del #{a['local']}/32 dev lo label SDNAT4" + end + + toggle [:save, :reload] + end +end +end diff --git a/appliances/VRouter/SDNAT4/main.rb b/appliances/VRouter/SDNAT4/main.rb new file mode 100644 index 00000000..6f2bb963 --- /dev/null +++ b/appliances/VRouter/SDNAT4/main.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require_relative '../vrouter.rb' +require_relative 'execute.rb' + +module Service +module SDNAT4 + extend self + + DEPENDS_ON = %w[Service::Failover] + + ONEAPP_VNF_SDNAT4_ENABLED = env :ONEAPP_VNF_SDNAT4_ENABLED, 'NO' + + ONEAPP_VNF_SDNAT4_REFRESH_RATE = env :ONEAPP_VNF_SDNAT4_REFRESH_RATE, '30' + + ONEAPP_VNF_SDNAT4_INTERFACES = env :ONEAPP_VNF_SDNAT4_INTERFACES, nil # nil -> none, empty -> all + + def install(initdir: '/etc/init.d') + msg :info, 'SDNAT4::install' + + puts bash 'apk --no-cache add iproute2 iptables-openrc ruby' + + file "#{initdir}/one-sdnat4", <<~SERVICE, mode: 'u=rwx,g=rx,o=' + #!/sbin/openrc-run + + source /run/one-context/one_env + + command="/usr/bin/ruby" + command_args="-r /etc/one-appliance/lib/helpers.rb -r #{__FILE__} -e Service::SDNAT4.execute" + + command_background="yes" + pidfile="/run/$RC_SVCNAME.pid" + + output_log="/var/log/one-appliance/one-sdnat4.log" + error_log="/var/log/one-appliance/one-sdnat4.log" + + depend() { + after net firewall keepalived + } + + stop_post() { + $command -r /etc/one-appliance/lib/helpers.rb -r #{__FILE__} -e Service::SDNAT4.cleanup 1>>$output_log 2>>$error_log + } + SERVICE + + toggle [:update] + end + + def configure + msg :info, 'SDNAT4::configure' + + if ONEAPP_VNF_SDNAT4_ENABLED + # Add dedicated SNAT4 chain. + puts bash(<<~IPTABLES) + iptables -t nat -nL SNAT4 || iptables -t nat -N SNAT4 + iptables -t nat -C POSTROUTING -j SNAT4 || iptables -t nat -I POSTROUTING 1 -j SNAT4 + IPTABLES + + # Add dedicated DNAT4 chain. + puts bash(<<~IPTABLES) + iptables -t nat -nL DNAT4 || iptables -t nat -N DNAT4 + iptables -t nat -C PREROUTING -j DNAT4 || iptables -t nat -I PREROUTING 1 -j DNAT4 + IPTABLES + + toggle [:save, :enable] + else + toggle [:stop, :disable] + end + end + + def toggle(operations) + operations.each do |op| + msg :info, "SDNAT4::toggle([:#{op}])" + case op + when :save + puts bash 'rc-service iptables save' + when :reload + puts bash 'rc-service iptables reload' + when :enable + puts bash 'rc-update add iptables default' + puts bash 'rc-update add one-sdnat4 default' + when :disable + puts bash 'rc-update del one-sdnat4 default ||:' + when :update + puts bash 'rc-update -u' + when :start + puts bash 'rc-service iptables start' + puts bash 'rc-service one-sdnat4 start' + else + puts bash "rc-service one-sdnat4 #{op.to_s}" + end + end + end + + def bootstrap + msg :info, 'SDNAT4::bootstrap' + end +end +end diff --git a/appliances/VRouter/SDNAT4/tests.rb b/appliances/VRouter/SDNAT4/tests.rb new file mode 100644 index 00000000..e173e187 --- /dev/null +++ b/appliances/VRouter/SDNAT4/tests.rb @@ -0,0 +1,186 @@ +# frozen_string_literal: true + +require 'ipaddr' +require 'rspec' +require 'tmpdir' + +def clear_vars(object) + object.instance_variables.each { |name| object.remove_instance_variable(name) } +end + +RSpec.describe self do + it 'should extract sdnat4 info from vnets' do + load './main.rb'; include Service::SDNAT4 + + allow(Service::SDNAT4).to receive(:ip_addr_show).and_return({ + 'ifname' => 'lo', + 'addr_info' => [ + { 'family' => 'inet', + 'local' => '127.0.0.1', + 'prefixlen' => 8, + 'label' => 'lo' }, + + { 'family' => 'inet', + 'local' => '10.2.11.202', + 'prefixlen' => 32, + 'label' => 'SDNAT4' }, + + # { 'family' => 'inet', + # 'local' => '10.2.11.203', + # 'prefixlen' => 32, + # 'label' => 'SDNAT4' } + ] + }) + + (vnets ||= []) << JSON.parse(<<~'VNET0') + { + "VNET": { + "ID": "0", + "NAME": "service", + "USED_LEASES": "6", + "VROUTERS": { + "ID": [ "35" ] + }, + "PARENT_NETWORK_ID": {}, + "AR_POOL": { + "AR": [ + { + "AR_ID": "0", + "IP": "10.2.11.200", + "MAC": "02:00:0a:02:0b:c8", + "SIZE": "48", + "TYPE": "IP4", + "MAC_END": "02:00:0a:02:0b:f7", + "IP_END": "10.2.11.247", + "USED_LEASES": "6", + "LEASES": { + "LEASE": [ + { "IP": "10.2.11.200", "MAC": "02:00:0a:02:0b:c8", "VM": "265", "NIC_NAME": "NIC0" }, + { "IP": "10.2.11.201", "MAC": "02:00:0a:02:0b:c9", "VM": "266", "NIC_NAME": "NIC0" }, + { + "IP": "10.2.11.202", + "MAC": "02:00:0a:02:0b:ca", + "VM": "267", + "PARENT": "NIC0", + "PARENT_NETWORK_ID": "1", + "EXTERNAL": true, + "NIC_NAME": "NIC0_ALIAS1" + }, + { + "IP": "10.2.11.203", + "MAC": "02:00:0a:02:0b:cb", + "VM": "268", + "PARENT": "NIC0", + "PARENT_NETWORK_ID": "1", + "EXTERNAL": true, + "NIC_NAME": "NIC0_ALIAS1" + }, + { "IP": "10.2.11.204", "MAC": "02:00:0a:02:0b:cc", "VM": "269", "NIC_NAME": "NIC0" }, + { "IP": "10.2.11.205", "MAC": "02:00:0a:02:0b:cd", "VM": "270", "NIC_NAME": "NIC0" } + ] + } + } + ] + }, + "TEMPLATE": { + "NETWORK_ADDRESS": "10.2.11.0", + "NETWORK_MASK": "255.255.255.0", + "GATEWAY": "10.2.11.1", + "DNS": "10.2.11.40" + } + } + } + VNET0 + (vnets ||= []) << JSON.parse(<<~'VNET1') + { + "VNET": { + "ID": "1", + "NAME": "private", + "USED_LEASES": "24", + "VROUTERS": { + "ID": [ "35" ] + }, + "PARENT_NETWORK_ID": {}, + "AR_POOL": { + "AR": [ + { + "AR_ID": "0", + "IP": "172.20.0.100", + "MAC": "02:00:ac:14:00:64", + "SIZE": "100", + "TYPE": "IP4", + "MAC_END": "02:00:ac:14:00:c7", + "IP_END": "172.20.0.199", + "USED_LEASES": "24", + "LEASES": { + "LEASE": [ + { "IP": "172.20.0.100", "MAC": "02:00:ac:14:00:64", "VNET": "40" }, + { "IP": "172.20.0.101", "MAC": "02:00:ac:14:00:65", "VNET": "40" }, + { "IP": "172.20.0.102", "MAC": "02:00:ac:14:00:66", "VNET": "40" }, + { "IP": "172.20.0.103", "MAC": "02:00:ac:14:00:67", "VNET": "40" }, + { "IP": "172.20.0.104", "MAC": "02:00:ac:14:00:68", "VNET": "40" }, + { "IP": "172.20.0.105", "MAC": "02:00:ac:14:00:69", "VNET": "40" }, + { "IP": "172.20.0.106", "MAC": "02:00:ac:14:00:6a", "VNET": "40" }, + { "IP": "172.20.0.107", "MAC": "02:00:ac:14:00:6b", "VNET": "40" }, + { "IP": "172.20.0.108", "MAC": "02:00:ac:14:00:6c", "VNET": "40" }, + { "IP": "172.20.0.109", "MAC": "02:00:ac:14:00:6d", "VNET": "40" }, + { "IP": "172.20.0.110", "MAC": "02:00:ac:14:00:6e", "VNET": "40" }, + { "IP": "172.20.0.111", "MAC": "02:00:ac:14:00:6f", "VNET": "40" }, + { "IP": "172.20.0.112", "MAC": "02:00:ac:14:00:70", "VNET": "40" }, + { "IP": "172.20.0.113", "MAC": "02:00:ac:14:00:71", "VNET": "40" }, + { "IP": "172.20.0.114", "MAC": "02:00:ac:14:00:72", "VNET": "40" }, + { "IP": "172.20.0.115", "MAC": "02:00:ac:14:00:73", "VNET": "40" }, + { "IP": "172.20.0.116", "MAC": "02:00:ac:14:00:74", "VNET": "40" }, + { "IP": "172.20.0.117", "MAC": "02:00:ac:14:00:75", "VNET": "40" }, + { "IP": "172.20.0.118", "MAC": "02:00:ac:14:00:76", "VNET": "40" }, + { "IP": "172.20.0.119", "MAC": "02:00:ac:14:00:77", "VNET": "40" }, + { "IP": "172.20.0.120", "MAC": "02:00:ac:14:00:78", "VM": "267", "NIC_NAME": "NIC0" }, + { "IP": "172.20.0.121", "MAC": "02:00:ac:14:00:79", "VM": "268", "NIC_NAME": "NIC0" }, + { "IP": "172.20.0.122", "MAC": "02:00:ac:14:00:7a", "VM": "269", "NIC_NAME": "NIC1" }, + { "IP": "172.20.0.123", "MAC": "02:00:ac:14:00:7b", "VM": "270", "NIC_NAME": "NIC1" } + ] + } + } + ] + }, + "TEMPLATE": { + "NETWORK_ADDRESS": "172.20.0.0", + "NETWORK_MASK": "255.255.255.0", + "GATEWAY": "172.20.0.86" + } + } + } + VNET1 + + clear_vars Service::SDNAT4 + + Service::SDNAT4.instance_variable_set(:@subnets, [ + IPAddr.new('10.2.11.0/24'), + IPAddr.new('172.20.0.0/16') + ]) + + expect(Service::SDNAT4.extract_external(vnets)).to eq ({ + external: [ + { 'EXTERNAL' => true, + 'IP' => '10.2.11.202', + 'MAC' => '02:00:0a:02:0b:ca', + 'NIC_NAME' => 'NIC0_ALIAS1', + 'PARENT' => 'NIC0', + 'PARENT_NETWORK_ID' => '1', + 'VM' => '267' }, + + { 'EXTERNAL' => true, + 'IP' => '10.2.11.203', + 'MAC' => '02:00:0a:02:0b:cb', + 'NIC_NAME' => 'NIC0_ALIAS1', + 'PARENT' => 'NIC0', + 'PARENT_NETWORK_ID' => '1', + 'VM' => '268' } + ], + ip_map: { '10.2.11.202' => '172.20.0.120', + '10.2.11.203' => '172.20.0.121' }, + to_del: [], + to_add: [ '10.2.11.203' ] + }) + end +end diff --git a/appliances/VRouter/tests.rb b/appliances/VRouter/tests.rb new file mode 100644 index 00000000..1345ca67 --- /dev/null +++ b/appliances/VRouter/tests.rb @@ -0,0 +1,950 @@ +# frozen_string_literal: true + +require 'json' +require 'rspec' +require_relative 'vrouter.rb' + +def clear_env + ENV.delete_if { |name| name.include?('VROUTER_') || name.include?('_VNF_') } +end + +RSpec.describe 'detect_vips' do + it 'should parse legacy variables' do + clear_env + ENV['ETH0_VROUTER_IP'] = '1.2.3.4' + ENV['ONEAPP_VROUTER_ETH0_VIP1'] = '2.3.4.5' + ENV['ONEAPP_VROUTER_ETH1_VIP0'] = '3.4.5.6' + expect(detect_vips).to eq ({ + 'eth0' => { 'ONEAPP_VROUTER_ETH0_VIP0' => '1.2.3.4', + 'ONEAPP_VROUTER_ETH0_VIP1' => '2.3.4.5' }, + 'eth1' => { 'ONEAPP_VROUTER_ETH1_VIP0' => '3.4.5.6' } + }) + end + + it 'should parse legacy variables with lower precedence' do + clear_env + ENV['ETH0_VROUTER_IP'] = '1.2.3.4' + ENV['ONEAPP_VROUTER_ETH0_VIP0'] = '2.3.4.5' + ENV['ETH1_VROUTER_IP'] = '3.4.5.6' + expect(detect_vips).to eq ({ + 'eth0' => { 'ONEAPP_VROUTER_ETH0_VIP0' => '2.3.4.5' }, + 'eth1' => { 'ONEAPP_VROUTER_ETH1_VIP0' => '3.4.5.6' } + }) + end +end + +RSpec.describe 'parse_interfaces' do + it 'should return empty interfaces with nil input' do + expect(parse_interfaces(nil)).to be_empty + end + + it 'should parse interfaces from a string' do + allow(self).to receive(:detect_nics).and_return([ + 'eth0', + 'eth1', + 'eth2', + 'eth3' + ]) + allow(self).to receive(:addrs_to_nics).and_return({ + '10.0.0.1' => ['eth0', 'eth2'], + '10.0.1.1' => ['eth1'] + }) + tests = [ + [ 'eth7/10.0.0.7 10.0.1.1@53', { 'eth1' => { name: 'eth1', addr: '10.0.1.1', port: '53' }, + 'eth7' => { name: 'eth7', addr: '10.0.0.7', port: nil } } ], + + [ '10.0.1.1@53', { 'eth1' => { name: 'eth1', addr: '10.0.1.1', port: '53' } } ], + + [ '10.0.0.1', { 'eth0' => { name: 'eth0', addr: '10.0.0.1', port: nil } } ], + + [ 'eth0/10.0.0.1@53 eth1/10.0.1.1@53', { 'eth0' => { name: 'eth0', addr: '10.0.0.1', port: '53' }, + 'eth1' => { name: 'eth1', addr: '10.0.1.1', port: '53' } } ], + + [ 'eth0/10.0.0.1 eth1/10.0.1.1', { 'eth0' => { name: 'eth0', addr: '10.0.0.1', port: nil }, + 'eth1' => { name: 'eth1', addr: '10.0.1.1', port: nil } } ], + + [ 'eth0 eth1,eth2;eth3', { 'eth0' => { name: 'eth0', addr: nil, port: nil }, + 'eth1' => { name: 'eth1', addr: nil, port: nil }, + 'eth2' => { name: 'eth2', addr: nil, port: nil }, + 'eth3' => { name: 'eth3', addr: nil, port: nil } } ], + + [ 'eth0/10.0.0.1@53', { 'eth0' => { name: 'eth0', addr: '10.0.0.1', port: '53' } } ], + + [ 'eth0/10.0.0.1@', { 'eth0' => { name: 'eth0', addr: '10.0.0.1', port: nil } } ], + + [ 'eth0/10.0.0.1', { 'eth0' => { name: 'eth0', addr: '10.0.0.1', port: nil } } ], + + [ 'eth0/', { 'eth0' => { name: 'eth0', addr: nil, port: nil } } ], + + [ 'eth0', { 'eth0' => { name: 'eth0', addr: nil, port: nil } } ], + + [ '', { 'eth0' => { name: 'eth0', addr: nil, port: nil }, + 'eth1' => { name: 'eth1', addr: nil, port: nil }, + 'eth2' => { name: 'eth2', addr: nil, port: nil }, + 'eth3' => { name: 'eth3', addr: nil, port: nil } } ] + ] + tests.each do |input, output| + expect(parse_interfaces(input)).to eq output + end + end + + it 'should parse interfaces from a string (negation)' do + allow(self).to receive(:detect_nics).and_return([ + 'eth0', + 'eth1', + 'eth2', + 'eth3' + ]) + allow(self).to receive(:addrs_to_nics).and_return({ + '10.0.0.1' => ['eth0', 'eth2'], + '10.0.1.1' => ['eth1'] + }) + tests = [ + [ 'eth0/10.0.0.1@53 eth1 eth2 !10.0.1.1', { 'eth0' => { name: 'eth0', addr: '10.0.0.1', port: '53' }, + 'eth2' => { name: 'eth2', addr: nil, port: nil } } ], + + [ '!eth1 10.0.1.1@53 eth3', { 'eth3' => { name: 'eth3', addr: nil, port: nil } } ], + + [ '10.0.1.1@53 eth3', { 'eth1' => { name: 'eth1', addr: '10.0.1.1', port: '53' }, + 'eth3' => { name: 'eth3', addr: nil, port: nil } } ], + + [ '!10.0.1.1', { 'eth0' => { name: 'eth0', addr: nil, port: nil }, + 'eth2' => { name: 'eth2', addr: nil, port: nil }, + 'eth3' => { name: 'eth3', addr: nil, port: nil } } ], + + [ '!eth0 !eth2', { 'eth1' => { name: 'eth1', addr: nil, port: nil }, + 'eth3' => { name: 'eth3', addr: nil, port: nil } } ], + + [ '!eth0', { 'eth1' => { name: 'eth1', addr: nil, port: nil }, + 'eth2' => { name: 'eth2', addr: nil, port: nil }, + 'eth3' => { name: 'eth3', addr: nil, port: nil } } ] + ] + tests.each do |input, output| + expect(parse_interfaces(input)).to eq output + end + end +end + +RSpec.describe 'render_interface' do + it 'should render interfaces from parts' do + tests = [ + [ { name: 'eth0', addr: nil , port: nil }, + { name: true , addr: false, port: false }, 'eth0' ], + + [ { name: 'eth0', addr: nil , port: nil }, + { name: false , addr: false, port: false }, 'eth0' ], + + [ { name: 'eth0', addr: '10.0.0.1', port: nil }, + { name: true , addr: false , port: false }, 'eth0' ], + + [ { name: 'eth0', addr: '10.0.0.1', port: nil }, + { name: true , addr: true , port: false }, 'eth0/10.0.0.1' ], + + [ { name: 'eth0', addr: '10.0.0.1', port: nil }, + { name: false , addr: true , port: false }, '10.0.0.1' ], + + [ { name: 'eth0', addr: '10.0.0.1', port: '53' }, + { name: true , addr: true , port: true }, 'eth0/10.0.0.1@53' ], + + [ { name: 'eth0', addr: '10.0.0.1', port: '53' }, + { name: true , addr: true , port: false }, 'eth0/10.0.0.1' ], + + [ { name: 'eth0', addr: '10.0.0.1', port: '53' }, + { name: true , addr: false , port: true }, 'eth0@53' ], + + [ { name: 'eth0', addr: '10.0.0.1', port: '53' }, + { name: false , addr: true , port: true }, '10.0.0.1@53' ] + ] + tests.each do |input, options, output| + expect(render_interface(input, **options)).to eq output + end + end +end + +RSpec.describe 'addrs_to_nics' do + it 'should map addrs to nics' do + allow(self).to receive(:ip_addr_list).and_return([ + { 'ifname' => 'eth0', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '10.0.1.1', + 'prefixlen' => 24 }, + { 'family' => 'inet', + 'local' => '10.0.1.2', + 'prefixlen' => 24 } ] }, + + { 'ifname' => 'eth1', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '172.16.1.1', + 'prefixlen' => 16 } ] }, + + { 'ifname' => 'eth2', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '172.18.1.1', + 'prefixlen' => 24 } ] }, + + { 'ifname' => 'eth3', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '172.18.1.1', + 'prefixlen' => 24 } ] } + ]) + tests = [ + [ %w[eth0], { '10.0.1.1' => %w[eth0], + '10.0.1.2' => %w[eth0] } ], + + [ %w[eth1 eth2 eth3], { '172.16.1.1' => %w[eth1], + '172.18.1.1' => %w[eth2 eth3] } ] + ] + tests.each do |input, output| + expect(addrs_to_nics(input)).to eq output + end + end + + it 'should map addrs to nics (:noip)' do + allow(self).to receive(:ip_addr_list).and_return([ + { 'ifname' => 'eth0', + 'addr_info' => [] }, + + { 'ifname' => 'eth1', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '172.16.1.1', + 'prefixlen' => 16 } ] }, + + { 'ifname' => 'eth2', + 'addr_info' => [] }, + + { 'ifname' => 'eth3', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '172.24.1.1', + 'prefixlen' => 16 } ] } + ]) + tests = [ + [ %w[eth0 eth1], { '172.16.1.1' => %w[eth1] } ], + + [ %w[eth0 eth1 eth2 eth3], { '172.16.1.1' => %w[eth1], + '172.24.1.1' => %w[eth3] } ] + ] + tests.each do |input, output| + expect(addrs_to_nics(input)).to eq output + end + end +end + +RSpec.describe 'addrs_to_subnets' do + it 'should extract subnets' do + allow(self).to receive(:ip_addr_list).and_return([ + { 'ifname' => 'eth0', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '10.0.1.1', + 'prefixlen' => 24 }, + { 'family' => 'inet', + 'local' => '10.0.1.2', + 'prefixlen' => 24 } ] }, + + { 'ifname' => 'eth1', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '172.16.1.1', + 'prefixlen' => 16 } ] }, + + { 'ifname' => 'eth2', + 'addr_info' => [ { 'family' => 'inet', + 'local' => '172.18.1.1', + 'prefixlen' => 24 } ] } + ]) + tests = [ + [ %w[eth0], { '10.0.1.1/24' => '10.0.1.0/24', + '10.0.1.2/24' => '10.0.1.0/24' } ], + + [ %w[eth1 eth2], { '172.16.1.1/16' => '172.16.0.0/16', + '172.18.1.1/24' => '172.18.1.0/24' } ] + ] + tests.each do |input, output| + expect(addrs_to_subnets(input)).to eq output + end + end +end + +RSpec.describe 'subnets_to_ranges' do + it 'should convert subnets to ranges' do + tests = [ + [ [ '172.16.0.0/16', '172.18.1.0/24' ], + { '172.16.0.0/16' => '172.16.0.2-172.16.255.254', + '172.18.1.0/24' => '172.18.1.2-172.18.1.254' } ], + + [ [ '2001:db8:1:0::/64', '2001:db8:1:1::/64' ], + { '2001:db8:1:0::/64' => '2001:db8:1::2-2001:db8:1:0:ffff:ffff:ffff:fffe', + '2001:db8:1:1::/64' => '2001:db8:1:1::2-2001:db8:1:1:ffff:ffff:ffff:fffe' } ] + ] + tests.each do |input, output| + expect(subnets_to_ranges(input)).to eq output + end + end +end + +RSpec.describe 'get_service_vms' do + it 'should list all available vms (oneflow)' do + allow(self).to receive(:onegate_service_show).and_return(JSON.parse(<<~'SERVICE_SHOW')) + { + "SERVICE": { + "name": "asd", + "id": "23", + "state": 1, + "roles": [ + { + "name": "server", + "cardinality": 2, + "state": 1, + "nodes": [ + { + "deploy_id": 435, + "running": null, + "vm_info": { + "VM": { + "ID": "435", + "UID": "0", + "GID": "0", + "UNAME": "oneadmin", + "GNAME": "oneadmin", + "NAME": "server_0_(service_23)" + } + } + }, + { + "deploy_id": 436, + "running": null, + "vm_info": { + "VM": { + "ID": "436", + "UID": "0", + "GID": "0", + "UNAME": "oneadmin", + "GNAME": "oneadmin", + "NAME": "server_1_(service_23)" + } + } + } + ] + } + ] + } + } + SERVICE_SHOW + (vms ||= []) << JSON.parse(<<~'VM0_SHOW') + { + "VM": { + "NAME": "server_0_(service_23)", + "ID": "435", + "STATE": "3", + "LCM_STATE": "3", + "USER_TEMPLATE": { + "HOT_RESIZE": { + "CPU_HOT_ADD_ENABLED": "NO", + "MEMORY_HOT_ADD_ENABLED": "NO" + }, + "LOGO": "images/logos/linux.png", + "LXD_SECURITY_PRIVILEGED": "true", + "MEMORY_UNIT_COST": "MB", + "ONEGATE_HAPROXY_LB0_IP": "10.2.11.86", + "ONEGATE_HAPROXY_LB0_PORT": "5432", + "ONEGATE_HAPROXY_LB0_SERVER_HOST": "10.2.11.202", + "ONEGATE_HAPROXY_LB0_SERVER_PORT": "2345", + "ROLE_NAME": "server", + "SERVICE_ID": "23" + }, + "TEMPLATE": { + "NIC": [ + { + "IP": "10.2.11.202", + "MAC": "02:00:0a:02:0b:ca", + "NAME": "_NIC0", + "NETWORK": "service" + }, + { + "IP": "172.20.0.122", + "MAC": "02:00:ac:14:00:7a", + "NAME": "_NIC1", + "NETWORK": "private" + } + ], + "NIC_ALIAS": [] + } + } + } + VM0_SHOW + (vms ||= []) << JSON.parse(<<~'VM1_SHOW') + { + "VM": { + "NAME": "server_1_(service_23)", + "ID": "436", + "STATE": "3", + "LCM_STATE": "3", + "USER_TEMPLATE": { + "HOT_RESIZE": { + "CPU_HOT_ADD_ENABLED": "NO", + "MEMORY_HOT_ADD_ENABLED": "NO" + }, + "LOGO": "images/logos/linux.png", + "LXD_SECURITY_PRIVILEGED": "true", + "MEMORY_UNIT_COST": "MB", + "ONEGATE_HAPROXY_LB0_IP": "10.2.11.86", + "ONEGATE_HAPROXY_LB0_PORT": "5432", + "ONEGATE_HAPROXY_LB0_SERVER_HOST": "10.2.11.203", + "ONEGATE_HAPROXY_LB0_SERVER_PORT": "2345", + "ROLE_NAME": "server", + "SERVICE_ID": "23" + }, + "TEMPLATE": { + "NIC": [ + { + "IP": "10.2.11.203", + "MAC": "02:00:0a:02:0b:cb", + "NAME": "_NIC0", + "NETWORK": "service" + }, + { + "IP": "172.20.0.123", + "MAC": "02:00:ac:14:00:7b", + "NAME": "_NIC1", + "NETWORK": "private" + } + ], + "NIC_ALIAS": [] + } + } + } + VM1_SHOW + + allow(self).to receive(:onegate_vm_show).and_return(*vms) + + expect(get_service_vms).to eq vms + end +end + +RSpec.describe 'get_vrouter_vnets' do + it 'should recursively resolve all viable vnets' do + allow(self).to receive(:onegate_vrouter_show).and_return(JSON.parse(<<~'VROUTER_SHOW')) + { + "VROUTER": { + "NAME": "vrouter", + "ID": "12", + "VMS": { + "ID": [ "115" ] + }, + "TEMPLATE": { + "NIC": [ + { + "NETWORK": "service", + "NETWORK_ID": "0", + "NIC_ID": "0" + }, + { + "NETWORK": "private", + "NETWORK_ID": "1", + "NIC_ID": "1" + } + ], + "TEMPLATE_ID": "74" + } + } + } + VROUTER_SHOW + + (vnets ||= []) << JSON.parse(<<~'SERVICE_VNET_SHOW') + { + "VNET": { + "ID": "0", + "NAME": "service", + "USED_LEASES": "4", + "VROUTERS": { + "ID": [ "12" ] + }, + "PARENT_NETWORK_ID": {}, + "AR_POOL": { + "AR": [ + { + "AR_ID": "0", + "IP": "10.2.11.200", + "MAC": "02:00:0a:02:0b:c8", + "SIZE": "48", + "TYPE": "IP4", + "MAC_END": "02:00:0a:02:0b:f7", + "IP_END": "10.2.11.247", + "USED_LEASES": "4", + "LEASES": { + "LEASE": [ + { "IP": "10.2.11.200", "MAC": "02:00:0a:02:0b:c8", "VM": "110", "NIC_NAME": "NIC0" }, + { "IP": "10.2.11.201", "MAC": "02:00:0a:02:0b:c9", "VM": "111", "NIC_NAME": "NIC0" }, + { + "IP": "10.2.11.202", + "MAC": "02:00:0a:02:0b:ca", + "VM": "113", + "PARENT": "NIC0", + "PARENT_NETWORK_ID": "40", + "EXTERNAL": true, + "NIC_NAME": "NIC0_ALIAS1" + }, + { "IP": "10.2.11.204", "MAC": "02:00:0a:02:0b:cc", "VM": "115", "NIC_NAME": "NIC0" } + ] + } + } + ] + }, + "TEMPLATE": { + "NETWORK_ADDRESS": "10.2.11.0", + "NETWORK_MASK": "255.255.255.0", + "GATEWAY": "10.2.11.1", + "DNS": "10.2.11.40" + } + } + } + SERVICE_VNET_SHOW + (vnets ||= []) << JSON.parse(<<~'PRIVATE_VNET_SHOW') + { + "VNET": { + "ID": "1", + "NAME": "private", + "USED_LEASES": "21", + "VROUTERS": { + "ID": [ "12" ] + }, + "PARENT_NETWORK_ID": {}, + "AR_POOL": { + "AR": [ + { + "AR_ID": "0", + "IP": "172.20.0.100", + "MAC": "02:00:ac:14:00:64", + "SIZE": "100", + "TYPE": "IP4", + "MAC_END": "02:00:ac:14:00:c7", + "IP_END": "172.20.0.199", + "USED_LEASES": "21", + "LEASES": { + "LEASE": [ + { "IP": "172.20.0.100", "MAC": "02:00:ac:14:00:64", "VNET": "40" }, + { "IP": "172.20.0.101", "MAC": "02:00:ac:14:00:65", "VNET": "40" }, + { "IP": "172.20.0.102", "MAC": "02:00:ac:14:00:66", "VNET": "40" }, + { "IP": "172.20.0.103", "MAC": "02:00:ac:14:00:67", "VNET": "40" }, + { "IP": "172.20.0.104", "MAC": "02:00:ac:14:00:68", "VNET": "40" }, + { "IP": "172.20.0.105", "MAC": "02:00:ac:14:00:69", "VNET": "40" }, + { "IP": "172.20.0.106", "MAC": "02:00:ac:14:00:6a", "VNET": "40" }, + { "IP": "172.20.0.107", "MAC": "02:00:ac:14:00:6b", "VNET": "40" }, + { "IP": "172.20.0.108", "MAC": "02:00:ac:14:00:6c", "VNET": "40" }, + { "IP": "172.20.0.109", "MAC": "02:00:ac:14:00:6d", "VNET": "40" }, + { "IP": "172.20.0.110", "MAC": "02:00:ac:14:00:6e", "VNET": "40" }, + { "IP": "172.20.0.111", "MAC": "02:00:ac:14:00:6f", "VNET": "40" }, + { "IP": "172.20.0.112", "MAC": "02:00:ac:14:00:70", "VNET": "40" }, + { "IP": "172.20.0.113", "MAC": "02:00:ac:14:00:71", "VNET": "40" }, + { "IP": "172.20.0.114", "MAC": "02:00:ac:14:00:72", "VNET": "40" }, + { "IP": "172.20.0.115", "MAC": "02:00:ac:14:00:73", "VNET": "40" }, + { "IP": "172.20.0.116", "MAC": "02:00:ac:14:00:74", "VNET": "40" }, + { "IP": "172.20.0.117", "MAC": "02:00:ac:14:00:75", "VNET": "40" }, + { "IP": "172.20.0.118", "MAC": "02:00:ac:14:00:76", "VNET": "40" }, + { "IP": "172.20.0.119", "MAC": "02:00:ac:14:00:77", "VNET": "40" }, + { "IP": "172.20.0.121", "MAC": "02:00:ac:14:00:79", "VM": "115", "NIC_NAME": "NIC1" } + ] + } + } + ] + }, + "TEMPLATE": { + "NETWORK_ADDRESS": "172.20.0.0", + "NETWORK_MASK": "255.255.255.0", + "GATEWAY": "172.20.0.86" + } + } + } + PRIVATE_VNET_SHOW + (vnets ||= []) << JSON.parse(<<~'RESERVATION_VNET_SHOW') + { + "VNET": { + "ID": "40", + "NAME": "reservation", + "USED_LEASES": "2", + "VROUTERS": { + "ID": [] + }, + "PARENT_NETWORK_ID": "1", + "AR_POOL": { + "AR": [ + { + "AR_ID": "0", + "IP": "172.20.0.100", + "MAC": "02:00:ac:14:00:64", + "PARENT_NETWORK_AR_ID": "0", + "SIZE": "20", + "TYPE": "IP4", + "MAC_END": "02:00:ac:14:00:77", + "IP_END": "172.20.0.119", + "USED_LEASES": "2", + "LEASES": { + "LEASE": [ + { "IP": "172.20.0.100", "MAC": "02:00:ac:14:00:64", "VM": "112", "NIC_NAME": "NIC0" }, + { "IP": "172.20.0.101", "MAC": "02:00:ac:14:00:65", "VM": "113", "NIC_NAME": "NIC0" } + ] + } + } + ] + }, + "TEMPLATE": { + "NETWORK_ADDRESS": "172.20.0.0", + "NETWORK_MASK": "255.255.255.0", + "GATEWAY": "172.20.0.86" + } + } + } + RESERVATION_VNET_SHOW + + allow(self).to receive(:onegate_vnet_show).and_return(*vnets) + + expect(get_vrouter_vnets).to eq vnets + end +end + +RSpec.describe 'backends.from_env' do + it 'should correctly extract backends from env vars' do + clear_env + + ENV['ONEAPP_VNF_LB0_IP'] = '10.2.11.86' + ENV['ONEAPP_VNF_LB0_PORT'] = '6969' + ENV['ONEAPP_VNF_LB0_PROTOCOL'] = 'TCP' + + ENV['ONEAPP_VNF_LB0_SERVER0_HOST'] = 'asd0' + ENV['ONEAPP_VNF_LB0_SERVER0_PORT'] = '1234' + ENV['ONEAPP_VNF_LB0_SERVER0_WEIGHT'] = '1' + + ENV['ONEAPP_VNF_LB0_SERVER1_HOST'] = 'asd1' + ENV['ONEAPP_VNF_LB0_SERVER1_PORT'] = '1234' + ENV['ONEAPP_VNF_LB0_SERVER1_WEIGHT'] = '2' + + ENV['ONEAPP_VNF_LB0_SERVER2_HOST'] = 'asd2' + ENV['ONEAPP_VNF_LB0_SERVER2_PORT'] = '1234' + ENV['ONEAPP_VNF_LB0_SERVER2_WEIGHT'] = '3' + + ENV['ONEAPP_VNF_LB1_IP'] = '10.2.11.86' + ENV['ONEAPP_VNF_LB1_PORT'] = '8686' + ENV['ONEAPP_VNF_LB1_PROTOCOL'] = 'TCP' + + ENV['ONEAPP_VNF_LB1_SERVER0_HOST'] = 'asd0' + ENV['ONEAPP_VNF_LB1_SERVER0_PORT'] = '4321' + ENV['ONEAPP_VNF_LB1_SERVER0_WEIGHT'] = '1' + + ENV['ONEAPP_VNF_LB1_SERVER1_HOST'] = 'asd1' + ENV['ONEAPP_VNF_LB1_SERVER1_PORT'] = '4321' + ENV['ONEAPP_VNF_LB1_SERVER1_WEIGHT'] = '2' + + expect(backends.from_env).to eq ({ + by_endpoint: { + [ 0, '10.2.11.86', '6969' ] => + { [ 'asd0', '1234' ] => { host: 'asd0', port: '1234', weight: '1' }, + [ 'asd1', '1234' ] => { host: 'asd1', port: '1234', weight: '2' }, + [ 'asd2', '1234' ] => { host: 'asd2', port: '1234', weight: '3' } }, + + [ 1, '10.2.11.86', '8686' ] => + { [ 'asd0', '4321' ] => { host: 'asd0', port: '4321', weight: '1' }, + [ 'asd1', '4321' ] => { host: 'asd1', port: '4321', weight: '2' } } }, + + options: { 0 => { ip: '10.2.11.86', port: '6969', protocol: 'TCP' }, + 1 => { ip: '10.2.11.86', port: '8686', protocol: 'TCP' } } + }) + end +end + +RSpec.describe 'backends.from_vnets' do + it 'should correctly extract backends from vnets' do + (vnets ||= []) << JSON.parse(<<~'VNET0') + { + "VNET": { + "ID": "0", + "AR_POOL": { + "AR": [ + { + "AR_ID": "0", + "LEASES": { + "LEASE": [ + { + "IP": "10.2.11.202", + "MAC": "02:00:0a:02:0b:ca", + "VM": "167", + "NIC_NAME": "NIC0", + "BACKEND": "YES", + + "ONEGATE_LB0_IP": "10.2.11.86", + "ONEGATE_LB0_PORT": "6969", + "ONEGATE_LB0_SERVER_HOST": "asd2", + "ONEGATE_LB0_SERVER_PORT": "1234", + "ONEGATE_LB0_SERVER_WEIGHT": "3" + }, + { + "IP": "10.2.11.201", + "MAC": "02:00:0a:02:0b:c9", + "VM": "167", + "NIC_NAME": "NIC0", + "BACKEND": "YES", + + "ONEGATE_LB0_IP": "10.2.11.86", + "ONEGATE_LB0_PORT": "6969", + "ONEGATE_LB0_SERVER_HOST": "asd1", + "ONEGATE_LB0_SERVER_PORT": "1234", + "ONEGATE_LB0_SERVER_WEIGHT": "2", + + "ONEGATE_LB1_IP": "10.2.11.86", + "ONEGATE_LB1_PORT": "8686", + "ONEGATE_LB1_SERVER_HOST": "asd1", + "ONEGATE_LB1_SERVER_PORT": "4321", + "ONEGATE_LB1_SERVER_WEIGHT": "2" + }, + { + "IP": "10.2.11.200", + "MAC": "02:00:0a:02:0b:c8", + "VM": "167", + "NIC_NAME": "NIC0", + "BACKEND": "YES", + + "ONEGATE_LB0_IP": "10.2.11.86", + "ONEGATE_LB0_PORT": "6969", + "ONEGATE_LB0_SERVER_HOST": "asd0", + "ONEGATE_LB0_SERVER_PORT": "1234", + "ONEGATE_LB0_SERVER_WEIGHT": "1", + + "ONEGATE_LB1_IP": "10.2.11.86", + "ONEGATE_LB1_PORT": "8686", + "ONEGATE_LB1_SERVER_HOST": "asd0", + "ONEGATE_LB1_SERVER_PORT": "4321", + "ONEGATE_LB1_SERVER_WEIGHT": "1" + } + ] + } + } + ] + } + } + } + VNET0 + expect(backends.from_vnets(vnets)).to eq ({ + by_endpoint: { + [ 0, '10.2.11.86', '6969' ] => + { [ 'asd0', '1234' ] => { host: 'asd0', port: '1234', weight: '1' }, + [ 'asd1', '1234' ] => { host: 'asd1', port: '1234', weight: '2' }, + [ 'asd2', '1234' ] => { host: 'asd2', port: '1234', weight: '3' } }, + + [ 1, '10.2.11.86', '8686' ] => + { [ 'asd0', '4321' ] => { host: 'asd0', port: '4321', weight: '1' }, + [ 'asd1', '4321' ] => { host: 'asd1', port: '4321', weight: '2' } } }, + + options: { 0 => { ip: '10.2.11.86', port: '6969' }, + 1 => { ip: '10.2.11.86', port: '8686' } } + }) + end +end + +RSpec.describe 'backends.from_vms' do + it 'should correctly extract backends from vms (oneflow)' do + (vms ||= []) << JSON.parse(<<~'VM0') + { + "VM": { + "NAME": "server_0_(service_23)", + "ID": "435", + "STATE": "3", + "LCM_STATE": "3", + "USER_TEMPLATE": { + "HOT_RESIZE": { + "CPU_HOT_ADD_ENABLED": "NO", + "MEMORY_HOT_ADD_ENABLED": "NO" + }, + "LOGO": "images/logos/linux.png", + "LXD_SECURITY_PRIVILEGED": "true", + "MEMORY_UNIT_COST": "MB", + "ONEGATE_LB0_IP": "10.2.11.86", + "ONEGATE_LB0_PORT": "5432", + "ONEGATE_LB0_SERVER_HOST": "10.2.11.202", + "ONEGATE_LB0_SERVER_PORT": "2345", + "ROLE_NAME": "server", + "SERVICE_ID": "23" + }, + "TEMPLATE": { + "NIC": [ + { + "IP": "10.2.11.202", + "MAC": "02:00:0a:02:0b:ca", + "NAME": "_NIC0", + "NETWORK": "service" + }, + { + "IP": "172.20.0.122", + "MAC": "02:00:ac:14:00:7a", + "NAME": "_NIC1", + "NETWORK": "private" + } + ], + "NIC_ALIAS": [] + } + } + } + VM0 + (vms ||= []) << JSON.parse(<<~'VM1') + { + "VM": { + "NAME": "server_1_(service_23)", + "ID": "436", + "STATE": "3", + "LCM_STATE": "3", + "USER_TEMPLATE": { + "HOT_RESIZE": { + "CPU_HOT_ADD_ENABLED": "NO", + "MEMORY_HOT_ADD_ENABLED": "NO" + }, + "LOGO": "images/logos/linux.png", + "LXD_SECURITY_PRIVILEGED": "true", + "MEMORY_UNIT_COST": "MB", + "ONEGATE_LB0_IP": "10.2.11.86", + "ONEGATE_LB0_PORT": "5432", + "ONEGATE_LB0_SERVER_HOST": "10.2.11.203", + "ONEGATE_LB0_SERVER_PORT": "2345", + "ROLE_NAME": "server", + "SERVICE_ID": "23" + }, + "TEMPLATE": { + "NIC": [ + { + "IP": "10.2.11.203", + "MAC": "02:00:0a:02:0b:cb", + "NAME": "_NIC0", + "NETWORK": "service" + }, + { + "IP": "172.20.0.123", + "MAC": "02:00:ac:14:00:7b", + "NAME": "_NIC1", + "NETWORK": "private" + } + ], + "NIC_ALIAS": [] + } + } + } + VM1 + + expect(backends.from_vms(vms)).to eq ({ + by_endpoint: { + [ 0, '10.2.11.86', '5432'] => + { [ '10.2.11.202', '2345' ] => { host: '10.2.11.202', port: '2345' }, + [ '10.2.11.203', '2345' ] => { host: '10.2.11.203', port: '2345' } } }, + + options: { 0 => { ip: '10.2.11.86', port: '5432' } } + }) + end +end + +RSpec.describe 'backends.intersect' do + it 'should extract only common endpoints (host/port pairs)' do + tests = [ + [ + { by_endpoint: { + [ 0, '10.2.11.86', '5432'] => + { [ '10.2.11.202', '2345' ] => { host: '10.2.11.202', port: '2345' } } }, + options: { 0 => { ip: '10.2.11.86', port: '5432' } } }, + + { by_endpoint: { + [ 0, '10.2.11.86', '1111'] => + { [ '10.2.11.202', '2345' ] => { host: '10.2.11.202', port: '2345' } } }, + options: { 0 => { ip: '10.2.11.86', port: '2345' } } }, + + { by_endpoint: {}, options: {} } + ], + [ + { by_endpoint: { + [ 0, '10.2.11.86', '5432'] => + { [ '10.2.11.202', '2345' ] => { host: '10.2.11.202', port: '2345' } } }, + options: { 0 => { ip: '10.2.11.86', port: '5432' } } }, + + { by_endpoint: { + [ 0, '10.2.11.86', '5432'] => + { [ '10.2.11.202', '2345' ] => { host: '10.2.11.202', port: '2345' } } }, + options: { 0 => { ip: '10.2.11.86', port: '5432' } } }, + + { by_endpoint: { + [ 0, '10.2.11.86', '5432'] => + { [ '10.2.11.202', '2345' ] => { host: '10.2.11.202', port: '2345' } } }, + options: { 0 => { ip: '10.2.11.86', port: '5432' } } } + ], + [ + { by_endpoint: { + [ 0, '10.2.11.86', '5432'] => + { [ '10.2.11.202', '2345' ] => { host: '10.2.11.202', port: '2345' } }, + [ 1, '10.2.11.86', '1111'] => + { [ '10.2.11.202', '2345' ] => { host: '10.2.11.202', port: '2345' } } }, + options: { 0 => { ip: '10.2.11.86', port: '5432' }, + 1 => { ip: '10.2.11.86', port: '1111' } } }, + + { by_endpoint: { + [ 0, '10.2.11.86', '5432'] => + { [ '10.2.11.202', '2345' ] => { host: '10.2.11.202', port: '2345' } } }, + options: { 0 => { ip: '10.2.11.86', port: '5432' } } }, + + { by_endpoint: { + [ 0, '10.2.11.86', '5432'] => + { [ '10.2.11.202', '2345' ] => { host: '10.2.11.202', port: '2345' } } }, + options: { 0 => { ip: '10.2.11.86', port: '5432' } } } + ], + [ + { by_endpoint: { + [ 0, '10.2.11.86', '5432'] => + { [ '10.2.11.202', '2345' ] => { host: '10.2.11.202', port: '2345' } } }, + options: { 0 => { ip: '10.2.11.86', port: '5432' } } }, + + { by_endpoint: { + [ 0, '10.2.11.86', '5432'] => + { [ '10.2.11.202', '2345' ] => { host: '10.2.11.202', port: '2345' } }, + [ 1, '10.2.11.86', '1111'] => + { [ '10.2.11.202', '2345' ] => { host: '10.2.11.202', port: '2345' } } }, + options: { 0 => { ip: '10.2.11.86', port: '5432' }, + 1 => { ip: '10.2.11.86', port: '1111' } } }, + + { by_endpoint: { + [ 0, '10.2.11.86', '5432'] => + { [ '10.2.11.202', '2345' ] => { host: '10.2.11.202', port: '2345' } } }, + options: { 0 => { ip: '10.2.11.86', port: '5432' } } } + ] + ] + tests.each do |a, b, output| + expect(backends.intersect(a, b)).to eq output + end + end +end + +RSpec.describe 'backends.resolve_vips' do + it 'should replace vip placeholders with existing vip ip addresses' do + tests = [ + [ + { 'eth0' => { 'ONEAPP_VROUTER_ETH0_VIP0' => '1.2.3.4', + 'ONEAPP_VROUTER_ETH0_VIP1' => '2.3.4.5' }, + 'eth1' => { 'ONEAPP_VROUTER_ETH1_VIP0' => '3.4.5.6' } }, + + { by_endpoint: { + [ 0, '', '5432'] => + { [ '10.2.11.202', '2345' ] => { host: '10.2.11.202', port: '2345' } }, + [ 1, '', '5432'] => + { [ '10.2.11.202', '2345' ] => { host: '10.2.11.202', port: '2345' } }, + [ 2, '4.5.6.7', '1111'] => + { [ '10.2.11.203', '2222' ] => { host: '10.2.11.203', port: '2222' } } }, + options: { 0 => { ip: '', port: '5432' }, + 1 => { ip: '', port: '5432' }, + 2 => { ip: '4.5.6.7', port: '1111' } } }, + + { by_endpoint: { + [ 0, '2.3.4.5', '5432'] => + { [ '10.2.11.202', '2345' ] => { host: '10.2.11.202', port: '2345' } }, + [ 1, '3.4.5.6', '5432'] => + { [ '10.2.11.202', '2345' ] => { host: '10.2.11.202', port: '2345' } }, + [ 2, '4.5.6.7', '1111'] => + { [ '10.2.11.203', '2222' ] => { host: '10.2.11.203', port: '2222' } } }, + options: { 0 => { ip: '2.3.4.5', port: '5432' }, + 1 => { ip: '3.4.5.6', port: '5432' }, + 2 => { ip: '4.5.6.7', port: '1111' } } } + ] + ] + tests.each do |vips, b, output| + expect(backends.resolve_vips(b, vips)).to eq output + end + end +end diff --git a/appliances/VRouter/tests.sh b/appliances/VRouter/tests.sh new file mode 100755 index 00000000..819172f9 --- /dev/null +++ b/appliances/VRouter/tests.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -eu -o pipefail; shopt -qs failglob + +find . -type f -name 'tests.rb' | while read FILE; do + (cd $(dirname "$FILE")/ && echo ">> $FILE <<" && rspec $(basename "$FILE")) +done diff --git a/appliances/VRouter/vrouter.rb b/appliances/VRouter/vrouter.rb new file mode 100644 index 00000000..54fd740b --- /dev/null +++ b/appliances/VRouter/vrouter.rb @@ -0,0 +1,399 @@ +# frozen_string_literal: true + +require 'ipaddr' +require 'json' + +begin + require '/etc/one-appliance/lib/helpers.rb' +rescue LoadError + require_relative '../lib/helpers.rb' +end + +def ip_link_list + stdout = bash 'ip --json link list', terminate: false + JSON.parse(stdout) +end + +def ip_link_show(nic) + stdout = bash "ip --json link show '#{nic}'", terminate: false + JSON.parse(stdout).first +end + +def ip_addr_list + stdout = bash 'ip --json addr list', terminate: false + JSON.parse(stdout) +end + +def ip_addr_show(nic) + stdout = bash "ip --json addr show '#{nic}'", terminate: false + JSON.parse(stdout).first +end + +def detect_nics(items = ip_link_list, pattern: /^eth\d+$/) + items.select { |nic| nic['ifname'] =~ pattern } + .map { |nic| nic['ifname'] } +end + +def detect_vips + ENV.each_with_object({}) do |(name, v), acc| + next if v.empty? + case name + when /^ETH(\d+)_VROUTER_IP$/ + acc["eth#{$1}"] ||= {} + acc["eth#{$1}"]["ONEAPP_VROUTER_ETH#{$1}_VIP0"] ||= v + when /^ONEAPP_VROUTER_ETH(\d+)_VIP\d+$/ + acc["eth#{$1}"] ||= {} + acc["eth#{$1}"][name] = v + end + end +end + +def detect_mgmt_interfaces + ENV.keys.select do |name| + name.start_with?('ETH') && name.end_with?('_VROUTER_MANAGEMENT') && env(name, 'NO') + end.map do |name| + name.split('_').first.downcase + end +end + +def parse_interfaces(interfaces, pattern: /^[!]?(lo|eth\d+)$/) + return {} if interfaces.nil? + + addrs = nil + + excluded, included = [], [] + + interfaces.split(%r{[ ,;]}).map(&:strip).compact.each do |interface| + if interface.start_with?(%[!]) + excluded << interface.delete_prefix(%[!]) if interface.size > 1 + else + included << interface if interface.size > 0 + end + end + + included = detect_nics if included.empty? + + excluded, included = [excluded, included].map do |collection| + collection.each_with_object({}) do |interface, acc| + parts = { name: nil, addr: nil, port: nil } + + interface.split(%r{(?=#{pattern.source}|[/@])}).each do |p| + case p + when pattern then parts[:name] = p + when %r{^/} then parts[:addr] = p.delete_prefix(%[/]) if p.size > 1 + when %r{^@} then parts[:port] = p.delete_prefix(%[@]) if p.size > 1 + else parts[:addr] = p + end + end + + if parts[:name].nil? + next if parts[:addr].nil? + + addrs ||= addrs_to_nics + parts[:name] = addrs[parts[:addr].downcase]&.first + end + + acc[parts[:name]] = parts + end + end + + included.select { |name, _| !name.nil? && !excluded.include?(name) } +end + +def render_interface(parts, name: false, addr: true, port: true) + tmp = [] + + tmp << parts[:name] if name || parts[:addr].nil? + + if addr && !parts[:addr].nil? + tmp << %[/] if name + tmp << parts[:addr] + end + + if port && !parts[:port].nil? + tmp << %[@] + tmp << parts[:port] + end + + tmp.join +end + +def addrs_to_nics(interfaces = detect_nics, family: %w[inet inet6]) + ip_addr_list.each_with_object({}) do |addr, acc| + next if addr['ifname'].nil? + next unless interfaces.include?(addr['ifname']) + + next if addr['addr_info'].nil? + + addr['addr_info'].each do |info| + next if info['family'].nil? + next unless family.include?(info['family'].downcase) + + next if info['local'].nil? + + (acc[info['local']] ||= []) << addr['ifname'] + end + end +end + +def addrs_to_subnets(interfaces = detect_nics, family: %w[inet inet6]) + ip_addr_list.each_with_object({}) do |addr, acc| + next if addr['ifname'].nil? + next unless interfaces.include?(addr['ifname']) + + next if addr['addr_info'].nil? + + addr['addr_info'].each do |info| + next if info['family'].nil? + next unless family.include?(info['family']) + + next if info['local'].nil? + + key = %[#{info['local']}/#{info['prefixlen']}] + + subnet = IPAddr.new(key) + + acc[key] = %[#{subnet}/#{subnet.prefix}] + end + end +end + +def subnets_to_ranges(subnets = addrs_to_subnets.values) + subnets.each_with_object({}) do |subnet, acc| + addr = IPAddr.new(subnet) + range = addr.to_range + acc[subnet] = [ + # Skip the network and the first usable address. + IPAddr.new(range.first.to_i + 2, addr.family).to_s, + # Skip the last address (broadcast). + IPAddr.new(range.last.to_i - 1, addr.family).to_s + ].join('-') + end +end + +def onegate_vrouter_show + stdout = bash 'onegate vrouter show --json --extended', terminate: false + JSON.parse(stdout) +rescue StandardError => e + msg :error, e.full_message + nil +end + +def onegate_vnet_show(network_id) + stdout = bash "onegate vnet show --json --extended '#{network_id}'", terminate: false + JSON.parse(stdout) +rescue StandardError => e + msg :error, e.full_message + nil +end + +def get_vrouter_vnets + return [] if (document = onegate_vrouter_show).nil? + return [] if (nics = document.dig('VROUTER', 'TEMPLATE', 'NIC')).nil? + + initial_network_ids = nics.map { |nic| nic['NETWORK_ID'] } + .compact + .uniq + + return [] if initial_network_ids.empty? + + def recurse(network_ids) + network_ids.each_with_object([]) do |network_id, vnets| + next if (vnet = onegate_vnet_show(network_id)).nil? + + vnets << vnet + + parent_network_id = vnet['PARENT_NETWORK_ID'] + + vnets << recurse([parent_network_id]) unless parent_network_id.nil? + + next if (ars = vnet.dig('VNET', 'AR_POOL', 'AR')).nil? + + ars.each do |ar| + next if (leases = ar.dig('LEASES', 'LEASE')).nil? + + parent_network_ids = leases.map { |lease| lease['VNET'] } + .compact + .uniq + + next if parent_network_ids.empty? + + vnets << recurse(parent_network_ids) + end + end.flatten.uniq + end + + recurse(initial_network_ids) +end + +def onegate_service_show + stdout = bash 'onegate service show --json', terminate: false + JSON.parse(stdout) +rescue StandardError => e + msg :error, e.full_message + nil +end + +def onegate_vm_show(vm_id) + stdout = bash "onegate vm show --json '#{vm_id}'", terminate: false + JSON.parse(stdout) +rescue StandardError => e + msg :error, e.full_message + nil +end + +def get_service_vms # OneFlow + return [] if (document = onegate_service_show).nil? + return [] if (roles = document.dig('SERVICE', 'roles')).nil? + + roles.each_with_object([]) do |role, acc| + next if (nodes = role.dig('nodes')).nil? + + nodes.each do |node| + next if (vm_id = node.dig('vm_info', 'VM', 'ID')).nil? + + acc << vm_id + end + end.uniq.each_with_object([]) do |vm_id, acc| + next if (vm = onegate_vm_show(vm_id)).nil? + + acc << vm + end +end + +def backends + def parse_static(names, prefix) + names.each_with_object({}) do |name, acc| + case name + when /^#{prefix}(\d+)_(IP|PORT|PROTOCOL|METHOD|SCHEDULER)$/ + lb_idx, opt = $1.to_i, $2 + key = lb_idx + acc[:options] ||= {} + acc[:options][key] ||= {} + acc[:options][key][opt.downcase.to_sym] = env(name, '') + when /^#{prefix}(\d+)_SERVER(\d+)_(HOST|PORT|WEIGHT|ULIMIT|LLIMIT)$/ + lb_idx, vm_idx, opt = $1.to_i, $2.to_i, $3 + key = [lb_idx, vm_idx] + acc[:by_indices] ||= {} + acc[:by_indices][key] ||= {} + acc[:by_indices][key][opt.downcase.to_sym] = env(name, '') + end + end.then do |doc| + doc[:by_indices]&.each do |(lb_idx, _), v| + key1 = [lb_idx, doc[:options][lb_idx][:ip], doc[:options][lb_idx][:port]] + next unless key1.all? + + key2 = [v[:host], v[:port]] + next unless key2.all? + + doc[:by_endpoint] ||= {} + doc[:by_endpoint][key1] ||= {} + doc[:by_endpoint][key1][key2] = v + end + doc.delete(:by_indices) + doc + end + end + + def parse_dynamic(objects, prefix) + objects.each_with_object({}) do |(name, v), acc| + case name + when /^#{prefix}(\d+)_(IP|PORT)$/ + lb_idx, opt = $1.to_i, $2 + key = lb_idx + acc[:options] ||= {} + acc[:options][key] ||= {} + acc[:options][key][opt.downcase.to_sym] = v + when /^#{prefix}(\d+)_SERVER_(HOST|PORT|WEIGHT|ULIMIT|LLIMIT)$/ + lb_idx, opt = $1.to_i, $2 + key = lb_idx + acc[:by_index] ||= {} + acc[:by_index][key] ||= {} + acc[:by_index][key][opt.downcase.to_sym] = v + end + end.then do |doc| + doc[:by_index]&.each do |lb_idx, v| + key1 = [lb_idx, doc[:options][lb_idx][:ip], doc[:options][lb_idx][:port]] + next unless key1.all? + + key2 = [v[:host], v[:port]] + next unless key2.all? + + doc[:by_endpoint] ||= {} + doc[:by_endpoint][key1] ||= {} + doc[:by_endpoint][key1][key2] = v + end + doc.delete(:by_index) + doc + end + end + + def from_env(prefix: 'ONEAPP_VNF_LB') # also 'ONEAPP_HAPROXY_VNF_LB' + parse_static(ENV.keys, prefix) + end + + def from_vnets(vnets, prefix: 'ONEGATE_LB') # also 'ONEGATE_HAPROXY_LB' + vnets.each_with_object({}) do |vnet, acc| + next if (ars = vnet.dig('VNET', 'AR_POOL', 'AR')).nil? + + ars.each do |ar| + next if (leases = ar.dig('LEASES', 'LEASE')).nil? + + leases.each do |lease| + next if lease['BACKEND'] != 'YES' + + hashmap.combine! acc, parse_dynamic(lease, prefix) + end + end + end + end + + def from_vms(vms, prefix: 'ONEGATE_LB') # also 'ONEGATE_HAPROXY_LB' + vms.each_with_object({}) do |vm, acc| + next if (user_template = vm.dig('VM', 'USER_TEMPLATE')).nil? + + hashmap.combine! acc, parse_dynamic(user_template, prefix) + end + end + + def intersect(a, b) + a[:by_endpoint] ||= {} + a[:options] ||= {} + + b[:by_endpoint] ||= {} + b[:options] ||= {} + + a_keys = a[:options].map { |lb_idx, opt| [lb_idx, opt[:ip], opt[:port]] } + b_keys = b[:options].map { |lb_idx, opt| [lb_idx, opt[:ip], opt[:port]] } + + keys = a_keys.intersection(b_keys) + + { by_endpoint: b[:by_endpoint].slice(*keys), + options: b[:options].slice(*keys.map { |key| key[0] }) } + end + + def resolve_vips(a, vips = detect_vips) + vips = vips.values.each_with_object({}) do |h, acc| + hashmap.combine! acc, h + end + + def interpolate(ip, vips) + if ip =~ /^<([A-Z_0-9]+)>$/ && !vips[$1].nil? + vips[$1].split('/')[0] # remove the CIDR prefix if present + else + ip + end + end + + { + by_endpoint: a[:by_endpoint].to_h do |(lb_idx, ip, port), v| + [ [lb_idx, interpolate(ip, vips), port], v ] + end, + + options: a[:options].to_h do |lb_idx, v| + v[:ip] = interpolate(v[:ip], vips) + [ lb_idx, v ] + end + } + end +end diff --git a/appliances/OneKE/appliance.sh b/appliances/legacy/OneKE/appliance.sh similarity index 100% rename from appliances/OneKE/appliance.sh rename to appliances/legacy/OneKE/appliance.sh diff --git a/appliances/OneKE/appliance/.rubocop.yml b/appliances/legacy/OneKE/appliance/.rubocop.yml similarity index 100% rename from appliances/OneKE/appliance/.rubocop.yml rename to appliances/legacy/OneKE/appliance/.rubocop.yml diff --git a/appliances/OneKE/appliance/appliance.rb b/appliances/legacy/OneKE/appliance/appliance.rb similarity index 100% rename from appliances/OneKE/appliance/appliance.rb rename to appliances/legacy/OneKE/appliance/appliance.rb diff --git a/appliances/OneKE/appliance/calico.rb b/appliances/legacy/OneKE/appliance/calico.rb similarity index 100% rename from appliances/OneKE/appliance/calico.rb rename to appliances/legacy/OneKE/appliance/calico.rb diff --git a/appliances/OneKE/appliance/canal.rb b/appliances/legacy/OneKE/appliance/canal.rb similarity index 100% rename from appliances/OneKE/appliance/canal.rb rename to appliances/legacy/OneKE/appliance/canal.rb diff --git a/appliances/OneKE/appliance/cilium.rb b/appliances/legacy/OneKE/appliance/cilium.rb similarity index 100% rename from appliances/OneKE/appliance/cilium.rb rename to appliances/legacy/OneKE/appliance/cilium.rb diff --git a/appliances/OneKE/appliance/cilium_spec.rb b/appliances/legacy/OneKE/appliance/cilium_spec.rb similarity index 100% rename from appliances/OneKE/appliance/cilium_spec.rb rename to appliances/legacy/OneKE/appliance/cilium_spec.rb diff --git a/appliances/OneKE/appliance/cleaner.rb b/appliances/legacy/OneKE/appliance/cleaner.rb similarity index 100% rename from appliances/OneKE/appliance/cleaner.rb rename to appliances/legacy/OneKE/appliance/cleaner.rb diff --git a/appliances/OneKE/appliance/cleaner_spec.rb b/appliances/legacy/OneKE/appliance/cleaner_spec.rb similarity index 100% rename from appliances/OneKE/appliance/cleaner_spec.rb rename to appliances/legacy/OneKE/appliance/cleaner_spec.rb diff --git a/appliances/OneKE/appliance/config.rb b/appliances/legacy/OneKE/appliance/config.rb similarity index 100% rename from appliances/OneKE/appliance/config.rb rename to appliances/legacy/OneKE/appliance/config.rb diff --git a/appliances/OneKE/appliance/helpers.rb b/appliances/legacy/OneKE/appliance/helpers.rb similarity index 100% rename from appliances/OneKE/appliance/helpers.rb rename to appliances/legacy/OneKE/appliance/helpers.rb diff --git a/appliances/OneKE/appliance/helpers_spec.rb b/appliances/legacy/OneKE/appliance/helpers_spec.rb similarity index 100% rename from appliances/OneKE/appliance/helpers_spec.rb rename to appliances/legacy/OneKE/appliance/helpers_spec.rb diff --git a/appliances/OneKE/appliance/kubernetes.rb b/appliances/legacy/OneKE/appliance/kubernetes.rb similarity index 100% rename from appliances/OneKE/appliance/kubernetes.rb rename to appliances/legacy/OneKE/appliance/kubernetes.rb diff --git a/appliances/OneKE/appliance/longhorn.rb b/appliances/legacy/OneKE/appliance/longhorn.rb similarity index 100% rename from appliances/OneKE/appliance/longhorn.rb rename to appliances/legacy/OneKE/appliance/longhorn.rb diff --git a/appliances/OneKE/appliance/metallb.rb b/appliances/legacy/OneKE/appliance/metallb.rb similarity index 100% rename from appliances/OneKE/appliance/metallb.rb rename to appliances/legacy/OneKE/appliance/metallb.rb diff --git a/appliances/OneKE/appliance/metallb_spec.rb b/appliances/legacy/OneKE/appliance/metallb_spec.rb similarity index 100% rename from appliances/OneKE/appliance/metallb_spec.rb rename to appliances/legacy/OneKE/appliance/metallb_spec.rb diff --git a/appliances/OneKE/appliance/multus.rb b/appliances/legacy/OneKE/appliance/multus.rb similarity index 100% rename from appliances/OneKE/appliance/multus.rb rename to appliances/legacy/OneKE/appliance/multus.rb diff --git a/appliances/OneKE/appliance/onegate.rb b/appliances/legacy/OneKE/appliance/onegate.rb similarity index 100% rename from appliances/OneKE/appliance/onegate.rb rename to appliances/legacy/OneKE/appliance/onegate.rb diff --git a/appliances/OneKE/appliance/onegate_spec.rb b/appliances/legacy/OneKE/appliance/onegate_spec.rb similarity index 100% rename from appliances/OneKE/appliance/onegate_spec.rb rename to appliances/legacy/OneKE/appliance/onegate_spec.rb diff --git a/appliances/OneKE/appliance/traefik.rb b/appliances/legacy/OneKE/appliance/traefik.rb similarity index 100% rename from appliances/OneKE/appliance/traefik.rb rename to appliances/legacy/OneKE/appliance/traefik.rb diff --git a/appliances/OneKE/appliance/vnf.rb b/appliances/legacy/OneKE/appliance/vnf.rb similarity index 100% rename from appliances/OneKE/appliance/vnf.rb rename to appliances/legacy/OneKE/appliance/vnf.rb diff --git a/appliances/lib/artifacts/vnf/ha-check-status.sh b/appliances/legacy/lib/artifacts/vnf/ha-check-status.sh similarity index 100% rename from appliances/lib/artifacts/vnf/ha-check-status.sh rename to appliances/legacy/lib/artifacts/vnf/ha-check-status.sh diff --git a/appliances/lib/artifacts/vnf/ha-failover.sh b/appliances/legacy/lib/artifacts/vnf/ha-failover.sh similarity index 100% rename from appliances/lib/artifacts/vnf/ha-failover.sh rename to appliances/legacy/lib/artifacts/vnf/ha-failover.sh diff --git a/appliances/lib/artifacts/vnf/kea-config-generator b/appliances/legacy/lib/artifacts/vnf/kea-config-generator similarity index 100% rename from appliances/lib/artifacts/vnf/kea-config-generator rename to appliances/legacy/lib/artifacts/vnf/kea-config-generator diff --git a/appliances/lib/artifacts/vnf/one-vnf/lib/appliance.rb b/appliances/legacy/lib/artifacts/vnf/one-vnf/lib/appliance.rb similarity index 100% rename from appliances/lib/artifacts/vnf/one-vnf/lib/appliance.rb rename to appliances/legacy/lib/artifacts/vnf/one-vnf/lib/appliance.rb diff --git a/appliances/lib/artifacts/vnf/one-vnf/lib/appliance/plugin.rb b/appliances/legacy/lib/artifacts/vnf/one-vnf/lib/appliance/plugin.rb similarity index 100% rename from appliances/lib/artifacts/vnf/one-vnf/lib/appliance/plugin.rb rename to appliances/legacy/lib/artifacts/vnf/one-vnf/lib/appliance/plugin.rb diff --git a/appliances/lib/artifacts/vnf/one-vnf/lib/appliance/plugin/dummy.rb b/appliances/legacy/lib/artifacts/vnf/one-vnf/lib/appliance/plugin/dummy.rb similarity index 100% rename from appliances/lib/artifacts/vnf/one-vnf/lib/appliance/plugin/dummy.rb rename to appliances/legacy/lib/artifacts/vnf/one-vnf/lib/appliance/plugin/dummy.rb diff --git a/appliances/lib/artifacts/vnf/one-vnf/lib/appliance/plugin/haproxy.rb b/appliances/legacy/lib/artifacts/vnf/one-vnf/lib/appliance/plugin/haproxy.rb similarity index 100% rename from appliances/lib/artifacts/vnf/one-vnf/lib/appliance/plugin/haproxy.rb rename to appliances/legacy/lib/artifacts/vnf/one-vnf/lib/appliance/plugin/haproxy.rb diff --git a/appliances/lib/artifacts/vnf/one-vnf/lib/appliance/plugin/loadbalancer.rb b/appliances/legacy/lib/artifacts/vnf/one-vnf/lib/appliance/plugin/loadbalancer.rb similarity index 100% rename from appliances/lib/artifacts/vnf/one-vnf/lib/appliance/plugin/loadbalancer.rb rename to appliances/legacy/lib/artifacts/vnf/one-vnf/lib/appliance/plugin/loadbalancer.rb diff --git a/appliances/lib/artifacts/vnf/one-vnf/lib/appliance/plugin/sdnat4.rb b/appliances/legacy/lib/artifacts/vnf/one-vnf/lib/appliance/plugin/sdnat4.rb similarity index 100% rename from appliances/lib/artifacts/vnf/one-vnf/lib/appliance/plugin/sdnat4.rb rename to appliances/legacy/lib/artifacts/vnf/one-vnf/lib/appliance/plugin/sdnat4.rb diff --git a/appliances/lib/artifacts/vnf/one-vnf/one-vnf.rb b/appliances/legacy/lib/artifacts/vnf/one-vnf/one-vnf.rb similarity index 100% rename from appliances/lib/artifacts/vnf/one-vnf/one-vnf.rb rename to appliances/legacy/lib/artifacts/vnf/one-vnf/one-vnf.rb diff --git a/appliances/legacy/lib/artifacts/vnf/onekea-2.2.0/kea-hook-onelease4-1.1.1-r0.apk b/appliances/legacy/lib/artifacts/vnf/onekea-2.2.0/kea-hook-onelease4-1.1.1-r0.apk new file mode 100644 index 00000000..c946778e Binary files /dev/null and b/appliances/legacy/lib/artifacts/vnf/onekea-2.2.0/kea-hook-onelease4-1.1.1-r0.apk differ diff --git a/appliances/lib/common.sh b/appliances/legacy/lib/common.sh similarity index 100% rename from appliances/lib/common.sh rename to appliances/legacy/lib/common.sh diff --git a/appliances/lib/context-helper.py b/appliances/legacy/lib/context-helper.py similarity index 100% rename from appliances/lib/context-helper.py rename to appliances/legacy/lib/context-helper.py diff --git a/appliances/lib/functions.sh b/appliances/legacy/lib/functions.sh similarity index 100% rename from appliances/lib/functions.sh rename to appliances/legacy/lib/functions.sh diff --git a/appliances/scripts/context_service_net-90.sh b/appliances/legacy/scripts/context_service_net-90.sh similarity index 100% rename from appliances/scripts/context_service_net-90.sh rename to appliances/legacy/scripts/context_service_net-90.sh diff --git a/appliances/scripts/context_service_net-99.sh b/appliances/legacy/scripts/context_service_net-99.sh similarity index 100% rename from appliances/scripts/context_service_net-99.sh rename to appliances/legacy/scripts/context_service_net-99.sh diff --git a/appliances/legacy/service b/appliances/legacy/service new file mode 100755 index 00000000..d02530af --- /dev/null +++ b/appliances/legacy/service @@ -0,0 +1,133 @@ +#!/usr/bin/env bash + +# ---------------------------------------------------------------------------- # +# Copyright 2018-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. # +# ---------------------------------------------------------------------------- # + +# USAGE: +# service [-h|--help|help] +# Print help and usage +# +# service install [] +# Download files and install packages for the desired version of a service +# +# service configure +# Configure the service via contextualization or with defaults +# +# service bootstrap +# Use user's predefined values for the final setup and start the service + +ONE_SERVICE_DIR=/etc/one-appliance +ONE_SERVICE_LOGDIR=/var/log/one-appliance +ONE_SERVICE_STATUS="${ONE_SERVICE_DIR}/status" +ONE_SERVICE_TEMPLATE="${ONE_SERVICE_DIR}/template" +ONE_SERVICE_METADATA="${ONE_SERVICE_DIR}/metadata" +ONE_SERVICE_REPORT="${ONE_SERVICE_DIR}/config" +ONE_SERVICE_FUNCTIONS="${ONE_SERVICE_DIR}/service.d/functions.sh" +ONE_SERVICE_COMMON="${ONE_SERVICE_DIR}/service.d/common.sh" +ONE_SERVICE_APPLIANCE="${ONE_SERVICE_DIR}/service.d/appliance.sh" +ONE_SERVICE_SETUP_DIR="/opt/one-appliance" +ONE_SERVICE_MOTD='/etc/motd' +ONE_SERVICE_PIDFILE="/var/run/one-appliance-service.pid" +ONE_SERVICE_CONTEXTFILE="${ONE_SERVICE_DIR}/context.json" +ONE_SERVICE_RECONFIGURE=false # the first time is always a full configuration +ONE_SERVICE_VERSION= # can be set by argument or to default +ONE_SERVICE_RECONFIGURABLE= # can be set by the appliance script + +# security precautions +set -e +umask 0077 + +# -> TODO: read all from ONE_SERVICE_DIR + +# source common functions +. "$ONE_SERVICE_COMMON" + +# source this script's functions +. "$ONE_SERVICE_FUNCTIONS" + +# source service appliance implementation (following functions): +# service_help +# service_install +# service_configure +# service_bootstrap +# service_cleanup +. "$ONE_SERVICE_APPLIANCE" + +# parse arguments and set _ACTION +_parse_arguments "$@" + +# execute requested action or fail +case "$_ACTION" in + nil|help) + # check if the appliance defined a help function + if type service_help >/dev/null 2>&1 ; then + # use custom appliance help + service_help + else + # use default + default_service_help + fi + ;; + badargs) + exit 1 + ;; + # all stages do basically this: + # 1. check status file if _ACTION can be run at all + # 2. set service status file + # 3. set motd (message of the day) + # 4. execute stage (install, configure or bootstrap) + # 5. set service status file again + # 6. set motd to normal or to signal failure + install|configure|bootstrap) + # check the status (am I running already) + if _is_running ; then + msg warning "Service script is running already - PID: $(_get_pid)" + exit 0 + fi + + # secure lock or fail (only one running instance of this script is allowed) + _lock_or_fail "$0" "$@" + + # set a trap for an exit (cleanup etc.) + _trap_exit + + # write a pidfile + _write_pid + + # analyze the current stage and either proceed or abort + if ! _check_service_status $_ACTION "$ONE_SERVICE_RECONFIGURABLE" ; then + exit 0 + fi + + # mark the start of a stage (install, configure or bootstrap) + _set_service_status $_ACTION + + # here we make sure that log directory exists + mkdir -p "$ONE_SERVICE_LOGDIR" + chmod 0700 "$ONE_SERVICE_LOGDIR" + + # execute action + _start_log "${ONE_SERVICE_LOGDIR}/ONE_${_ACTION}.log" + service_${_ACTION} 2>&1 + _end_log + + # if we reached this point then the current stage was successfull + _set_service_status success + ;; +esac + +exit 0 + diff --git a/appliances/vnf.sh b/appliances/legacy/vnf.sh similarity index 100% rename from appliances/vnf.sh rename to appliances/legacy/vnf.sh diff --git a/appliances/wordpress.sh b/appliances/legacy/wordpress.sh similarity index 100% rename from appliances/wordpress.sh rename to appliances/legacy/wordpress.sh diff --git a/appliances/lib/helpers.rb b/appliances/lib/helpers.rb new file mode 100644 index 00000000..d22003a9 --- /dev/null +++ b/appliances/lib/helpers.rb @@ -0,0 +1,219 @@ +# frozen_string_literal: true + +require 'base64' +require 'fileutils' +require 'ipaddr' +require 'json' +require 'logger' +require 'open3' +require 'socket' + +LOGGER_STDOUT = Logger.new(STDOUT) +LOGGER_STDERR = Logger.new(STDERR) + +LOGGERS = { + info: LOGGER_STDOUT.method(:info), + debug: LOGGER_STDERR.method(:debug), + warn: LOGGER_STDERR.method(:warn), + error: LOGGER_STDERR.method(:error) +}.freeze + +def msg(level, string) + LOGGERS[level].call string +end + +def env(name, default) + value = ENV.fetch name.to_s, '' + value = value.empty? ? default : value + value = %w[YES 1].include?(value.upcase) if default.instance_of?(String) && %w[YES NO].include?(default.upcase) + value +end + +def load_env(path = '/run/one-context/one_env') + File.read(path).lines.each do |line| + line.strip! + next if line.empty? + + line.delete_prefix!('export ') + + k, v = line.split('=', 2) + next if v.nil? + + ENV[k] = v.undump + end +end + +def slurp(path) + Base64.encode64(File.read(path)).lines.map(&:strip).join +end + +def file(path, content, owner: nil, group: nil, mode: 'u=rw,go=r', overwrite: false) + return if !overwrite && File.exist?(path) + + FileUtils.mkdir_p File.dirname path + + File.write path, content + + FileUtils.chown owner, group, path unless owner.nil? || group.nil? + + FileUtils.chmod mode, path +end + +def bash(script, chomp: false, terminate: false) + command = 'exec /bin/bash --login -s' + + stdin_data = <<~SCRIPT + set -o errexit -o nounset -o pipefail + set -x + #{script} + SCRIPT + + stdout, stderr, status = Open3.capture3 command, stdin_data: stdin_data + unless status.exitstatus.zero? + error_message = "#{status.exitstatus}: #{stderr}" + msg :error, error_message + + raise error_message unless terminate + + exit status.exitstatus + end + + chomp ? stdout.chomp : stdout +end + +def ipv4?(string) + string.is_a?(String) && IPAddr.new(string) ? true : false +rescue IPAddr::InvalidAddressError + false +end + +def integer?(string) + Integer(string) ? true : false +rescue ArgumentError + false +end + +alias port? integer? + +def tcp_port_open?(ipv4, port, seconds = 5) + # > If a block is given, the block is called with the socket. + # > The value of the block is returned. + # > The socket is closed when this method returns. + Socket.tcp(ipv4, port, connect_timeout: seconds) {} + true +rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH, Errno::ETIMEDOUT + false +end + +def hashmap + def recurse(a, b, g) + return a.method(g.next).call(b) { |_, a, b| recurse(a, b, g) } if a.is_a?(Hash) && b.is_a?(Hash) + return b + end + + # USAGE: c = hashmap.combine a, b + def combine(a, b) + recurse(a, b, Enumerator.new { |y| loop { y << :merge } }) + end + + # USAGE: hashmap.combine! a, b + def combine!(a, b) + recurse(a, b, Enumerator.new { |y| y << :merge!; loop { y << :merge } }) + end +end + +def sorted_deps(deps) + # NOTE: This doesn't handle circular dependencies. + + # Work with string keys only. + d = deps.to_h { |k, v| [k.to_s, v.map(&:to_s)] } + + def recurse(d, x, level = 0) + # The distance is at least the same as the current level. + distance = level + + # Recurse down each branch and record the longest distance to the root. + d[x].each { |y| distance = [distance, recurse(d, y, level + 1)].max } + + distance + end + + deps.keys.map { |k| [k, recurse(d, k.to_s)] } # compute the longest distance + .sort_by(&:last) # sort by the distance + .map(&:first) # return sorted keys (original) +end + +# install|configure|bootstrap started|success|failure +def set_motd(step, status, path = '/etc/motd') + header_txt = <<~'HEADER' + . + ___ _ __ ___ + / _ \ | '_ \ / _ \ OpenNebula Service Appliance + | (_) || | | || __/ + \___/ |_| |_| \___| + + HEADER + + step_txt = \ + case step.to_sym + when :install then '1/3 Installation' + when :configure then '2/3 Configuration' + when :bootstrap then '3/3 Bootstrap' + end + + status_txt = \ + case status.to_sym + when :started then <<~STARTED + #{header_txt} + #{step_txt} step is in progress... + + * * * * * * * * + * PLEASE WAIT * + * * * * * * * * + + STARTED + when :success then if step.to_sym == :bootstrap + <<~SUCCESS + #{header_txt} + All set and ready to serve 8) + + SUCCESS + else + <<~SUCCESS + #{header_txt} + #{step_txt} step was successfull. + + SUCCESS + end + when :failure then <<~FAILURE + #{header_txt} + #{step_txt} step failed. + + * * * * * * * * * * + * APPLIANCE ERROR * + * * * * * * * * * * + + Read documentation and try to redeploy! + + FAILURE + end + + file path, status_txt.delete_prefix('.'), mode: 'u=rw,go=r', overwrite: true +end + +# install|configure|bootstrap|success|failure +def set_status(status, path = '/etc/one-appliance/status') + case status.to_sym + when :install, :configure, :bootstrap + file path, <<~STATUS, mode: 'u=rw,go=r', overwrite: true + #{status.to_s}_started + STATUS + set_motd status, :started + when :success, :failure + step = File.open(path, &:gets).strip.split('_').first + file path, <<~STATUS, mode: 'u=rw,go=r', overwrite: true + #{step}_#{status.to_s} + STATUS + set_motd step, status + end +end diff --git a/appliances/lib/tests.rb b/appliances/lib/tests.rb new file mode 100644 index 00000000..b912ab4a --- /dev/null +++ b/appliances/lib/tests.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +require 'rspec' +require 'tmpdir' +require_relative 'helpers.rb' + +RSpec.describe 'load_env' do + it 'should load env vars from file' do + tests = [ + [ { :E1 => 'V1', + :E2 => 'V2', + :E3 => 'V3' }, + <<~INPUT + export E1="V1" + export E2="V2" + export E3="V3" + INPUT + ], + [ { :E1 => '"', + :E2 => "\n", + :E3 => "\\n" }, + <<~'INPUT' + export E1="\"" + export E2="\n" + export E3="\\n" + INPUT + ], + ] + Dir.mktmpdir do |dir| + tests.each do |output, input| + File.write "#{dir}/one_env", input + load_env "#{dir}/one_env" + output.each do |k, v| + expect(ENV[k.to_s]).to eq v + end + end + end + end +end + +RSpec.describe 'bash' do + it 'should raise' do + allow(self).to receive(:exit).and_return nil + expect { bash 'false' }.to raise_error(RuntimeError) + end + it 'should not raise' do + allow(self).to receive(:exit).and_return nil + expect { bash 'false', terminate: true }.not_to raise_error + end +end + +RSpec.describe 'ipv4?' do + it 'should evaluate to true' do + ipv4s = %w[ + 10.11.12.13 + 10.11.12.13/24 + 10.11.12.13/32 + 192.168.144.120 + ] + ipv4s.each do |item| + expect(ipv4?(item)).to be true + end + end + it 'should evaluate to false' do + ipv4s = %w[ + 10.11.12 + 10.11.12. + 10.11.12.256 + asd.168.144.120 + 192.168.144.96-192.168.144.120 + ] + ipv4s.each do |item| + expect(ipv4?(item)).to be false + end + end +end + +RSpec.describe 'hashmap' do + tests = [ + [ [{}, {}], {} ], + + [ [{a: 1}, {b: 2}], {a: 1, b: 2} ], + + [ [{a: 1, b: 3}, {b: 2}], {a: 1, b: 2} ], + + [ [{a: 1, b: 2}, {b: []}], {a: 1, b: []} ], + + [ [{a: 1, b: [:c]}, {b: []}], {a: 1, b: []} ], + + [ [{a: 1, b: {c: 3, d: 3}}, {b: {c: 2, e: 4}}], {a: 1, b: {c: 2, d: 3, e: 4}} ] + ] + it 'should recursively combine two hashmaps' do + tests.each do |(a, b), c| + expect(hashmap.combine(a, b)).to eq c + end + end + it 'should recursively combine two hashmaps (in-place)' do + tests.each do |(a, b), c| + hashmap.combine!(a, b) + expect(a).to eq c + end + end +end + +RSpec.describe 'sorted_deps' do + it 'should sort dependencies' do + tests = [ + [ { :a => [:b], + :b => [:c], + :c => [:d], + :d => [] }, [:d, :c, :b, :a] ], + + [ { :d => [:b], + :c => [:b, :d], + :b => [:a], + :a => [] }, [:a, :b, :d, :c] ], + + [ + { + :Failover => [:Keepalived], + :NAT4 => [:Failover, :Router4], + :Keepalived => [], + :Router4 => [:Failover] + }, + [ + :Keepalived, + :Failover, + :Router4, + :NAT4 + ] + ] + ] + tests.each do |input, output| + expect(sorted_deps(input)).to eq output + end + end +end + +RSpec.describe 'set_motd' do + it 'should render motd' do + output = <<~'OUTPUT' + . + ___ _ __ ___ + / _ \ | '_ \ / _ \ OpenNebula Service Appliance + | (_) || | | || __/ + \___/ |_| |_| \___| + + + All set and ready to serve 8) + + OUTPUT + Dir.mktmpdir do |dir| + set_motd :bootstrap, :success, "#{dir}/motd" + result = File.read "#{dir}/motd" + expect(result).to eq output.delete_prefix('.') + end + end +end + +RSpec.describe 'set_status' do + it 'should set status' do + allow(self).to receive(:set_motd).and_return nil + tests = [ + [ :install, 'install_started' ], + [ :success, 'install_success' ], + [ :configure, 'configure_started' ], + [ :success, 'configure_success' ], + [ :bootstrap, 'bootstrap_started' ], + [ :failure, 'bootstrap_failure' ] + ] + Dir.mktmpdir do |dir| + tests.each do |input, output| + set_status input, "#{dir}/status" + result = File.open("#{dir}/status", &:gets).strip + expect(result).to eq output + end + end + end +end diff --git a/appliances/lib/tests.sh b/appliances/lib/tests.sh new file mode 100755 index 00000000..819172f9 --- /dev/null +++ b/appliances/lib/tests.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -eu -o pipefail; shopt -qs failglob + +find . -type f -name 'tests.rb' | while read FILE; do + (cd $(dirname "$FILE")/ && echo ">> $FILE <<" && rspec $(basename "$FILE")) +done diff --git a/appliances/scripts/net-90 b/appliances/scripts/net-90 new file mode 100755 index 00000000..f09075ed --- /dev/null +++ b/appliances/scripts/net-90 @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Runs OpenNebula service appliances configuration & bootstrap script + +set -o errexit + +[[ -x /etc/one-appliance/service ]] && /etc/one-appliance/service configure bootstrap diff --git a/appliances/scripts/net-99 b/appliances/scripts/net-99 new file mode 100755 index 00000000..b60ad38f --- /dev/null +++ b/appliances/scripts/net-99 @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +: "${ENV_FILE:=/var/run/one-context/one_env}" + +set -o errexit + +if [[ "$REPORT_READY" != "YES" ]]; then + exit +fi + +# $TOKENTXT is available only through the env. file +if [[ -f "${ENV_FILE}" ]]; then + . "${ENV_FILE}" +fi + +# Reports only if ONE service appliance bootstrapped successfully +if [[ -x '/etc/one-appliance/service' ]]; then + _STATUS=$(cat '/etc/one-appliance/status' 2>/dev/null) + if [[ "$_STATUS" != 'bootstrap_success' ]]; then + exit + fi +fi + +### + +if which onegate >/dev/null 2>&1; then + if onegate vm update --data READY=YES; then + exit + fi +fi + +if which curl >/dev/null 2>&1; then + if curl -X PUT "$ONEGATE_ENDPOINT/vm" \ + --header "X-ONEGATE-TOKEN: $TOKENTXT" \ + --header "X-ONEGATE-VMID: $VMID" \ + -d READY=YES; then + exit + fi +fi + +if which wget >/dev/null 2>&1; then + if wget --method PUT "$ONEGATE_ENDPOINT/vm" \ + --header "X-ONEGATE-TOKEN: $TOKENTXT" \ + --header "X-ONEGATE-VMID: $VMID" \ + --body-data READY=YES; then + exit + fi +fi diff --git a/appliances/service b/appliances/service index d02530af..f745c751 100755 --- a/appliances/service +++ b/appliances/service @@ -1,133 +1,84 @@ -#!/usr/bin/env bash - -# ---------------------------------------------------------------------------- # -# Copyright 2018-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. # -# ---------------------------------------------------------------------------- # - -# USAGE: -# service [-h|--help|help] -# Print help and usage -# -# service install [] -# Download files and install packages for the desired version of a service -# -# service configure -# Configure the service via contextualization or with defaults -# -# service bootstrap -# Use user's predefined values for the final setup and start the service - -ONE_SERVICE_DIR=/etc/one-appliance -ONE_SERVICE_LOGDIR=/var/log/one-appliance -ONE_SERVICE_STATUS="${ONE_SERVICE_DIR}/status" -ONE_SERVICE_TEMPLATE="${ONE_SERVICE_DIR}/template" -ONE_SERVICE_METADATA="${ONE_SERVICE_DIR}/metadata" -ONE_SERVICE_REPORT="${ONE_SERVICE_DIR}/config" -ONE_SERVICE_FUNCTIONS="${ONE_SERVICE_DIR}/service.d/functions.sh" -ONE_SERVICE_COMMON="${ONE_SERVICE_DIR}/service.d/common.sh" -ONE_SERVICE_APPLIANCE="${ONE_SERVICE_DIR}/service.d/appliance.sh" -ONE_SERVICE_SETUP_DIR="/opt/one-appliance" -ONE_SERVICE_MOTD='/etc/motd' -ONE_SERVICE_PIDFILE="/var/run/one-appliance-service.pid" -ONE_SERVICE_CONTEXTFILE="${ONE_SERVICE_DIR}/context.json" -ONE_SERVICE_RECONFIGURE=false # the first time is always a full configuration -ONE_SERVICE_VERSION= # can be set by argument or to default -ONE_SERVICE_RECONFIGURABLE= # can be set by the appliance script - -# security precautions -set -e -umask 0077 - -# -> TODO: read all from ONE_SERVICE_DIR - -# source common functions -. "$ONE_SERVICE_COMMON" - -# source this script's functions -. "$ONE_SERVICE_FUNCTIONS" - -# source service appliance implementation (following functions): -# service_help -# service_install -# service_configure -# service_bootstrap -# service_cleanup -. "$ONE_SERVICE_APPLIANCE" - -# parse arguments and set _ACTION -_parse_arguments "$@" - -# execute requested action or fail -case "$_ACTION" in - nil|help) - # check if the appliance defined a help function - if type service_help >/dev/null 2>&1 ; then - # use custom appliance help - service_help - else - # use default - default_service_help - fi - ;; - badargs) - exit 1 - ;; - # all stages do basically this: - # 1. check status file if _ACTION can be run at all - # 2. set service status file - # 3. set motd (message of the day) - # 4. execute stage (install, configure or bootstrap) - # 5. set service status file again - # 6. set motd to normal or to signal failure - install|configure|bootstrap) - # check the status (am I running already) - if _is_running ; then - msg warning "Service script is running already - PID: $(_get_pid)" - exit 0 - fi +#!/usr/bin/env ruby + +# frozen_string_literal: true + +begin + require '/etc/one-appliance/lib/helpers.rb' +rescue LoadError + require_relative './lib/helpers.rb' +end + +require 'optparse' + +STEPS = %w[install configure bootstrap] + +SERVICE_D = File.join File.dirname(__FILE__), 'service.d' - # secure lock or fail (only one running instance of this script is allowed) - _lock_or_fail "$0" "$@" +SERVICE_LOGDIR = '/var/log/one-appliance' - # set a trap for an exit (cleanup etc.) - _trap_exit +def include_services(service_d = SERVICE_D) + Dir[File.join service_d, '**/main.rb'].each do |path| + require path + end - # write a pidfile - _write_pid + before = Module.constants - # analyze the current stage and either proceed or abort - if ! _check_service_status $_ACTION "$ONE_SERVICE_RECONFIGURABLE" ; then + include Service + + after = Module.constants + + (after - before).sort.map do |constant| + Module.const_get constant + end +end + +if caller.empty? + steps = <<~STEPS + Steps: + #{STEPS.join(' ')} + STEPS + + parser = OptionParser.new do |opts| + opts.banner = "Usage: #{$PROGRAM_NAME} [options] step1 step2 ..." + opts.separator 'Options:' + opts.on_tail('-h', '--help', 'Show help message') do + puts opts + puts steps exit 0 - fi + end + end + parser.parse! + + if ARGV.empty? || !(ARGV - STEPS).empty? + puts parser.help + puts steps + exit 1 + end + + Dir.mkdir(SERVICE_LOGDIR, 0750) unless File.exist?(SERVICE_LOGDIR) - # mark the start of a stage (install, configure or bootstrap) - _set_service_status $_ACTION + stdout, stderr = $stdout.dup, $stderr.dup - # here we make sure that log directory exists - mkdir -p "$ONE_SERVICE_LOGDIR" - chmod 0700 "$ONE_SERVICE_LOGDIR" + services = sorted_deps(include_services.to_h { |s| [s, s.const_get(:DEPENDS_ON)] }) - # execute action - _start_log "${ONE_SERVICE_LOGDIR}/ONE_${_ACTION}.log" - service_${_ACTION} 2>&1 - _end_log + ARGV.product(services).each do |step, service| + set_status step - # if we reached this point then the current stage was successfull - _set_service_status success - ;; -esac + open File.join(SERVICE_LOGDIR, "#{step}.log"), 'a' do |logfile| + $stdout.reopen logfile + $stderr.reopen logfile + service.method(step).call + rescue StandardError => e + stderr.puts e.full_message + stderr.flush + raise e + ensure + $stdout.flush + $stderr.flush + end + end -exit 0 + $stdout, $stderr = stdout, stderr + set_status :success +end diff --git a/packer/service_OneKE/OneKE.pkr.hcl b/packer/service_OneKE/OneKE.pkr.hcl index 2dfceb66..c2614c06 100644 --- a/packer/service_OneKE/OneKE.pkr.hcl +++ b/packer/service_OneKE/OneKE.pkr.hcl @@ -68,37 +68,37 @@ build { } provisioner "file" { - source = "appliances/scripts/context_service_net-90.sh" + source = "appliances/legacy/scripts/context_service_net-90.sh" destination = "/etc/one-appliance/net-90" } provisioner "file" { - source = "appliances/scripts/context_service_net-99.sh" + source = "appliances/legacy/scripts/context_service_net-99.sh" destination = "/etc/one-appliance/net-99" } provisioner "file" { - source = "appliances/service" + source = "appliances/legacy/service" destination = "/etc/one-appliance/service" } provisioner "file" { - source = "appliances/lib/common.sh" + source = "appliances/legacy/lib/common.sh" destination = "/etc/one-appliance/service.d/common.sh" } provisioner "file" { - source = "appliances/lib/functions.sh" + source = "appliances/legacy/lib/functions.sh" destination = "/etc/one-appliance/service.d/functions.sh" } provisioner "file" { - source = "appliances/lib/context-helper.py" + source = "appliances/legacy/lib/context-helper.py" destination = "/opt/one-appliance/bin/context-helper" } provisioner "file" { - source = "appliances/OneKE/" + source = "appliances/legacy/OneKE/" destination = "/etc/one-appliance/service.d/" } diff --git a/packer/service_VRouter/10-update.sh b/packer/service_VRouter/10-update.sh new file mode 100644 index 00000000..f7f92812 --- /dev/null +++ b/packer/service_VRouter/10-update.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh + +# (Auto)Removes unneeded packages and upgrades +# the distro. + +exec 1>&2 +set -ex + +service haveged stop ||: + +apk update + +apk --no-cache add \ + bash curl ethtool gawk grep iproute2 jq ruby sed tcpdump + +sync diff --git a/packer/service_VRouter/81-configure-ssh.sh b/packer/service_VRouter/81-configure-ssh.sh new file mode 100644 index 00000000..7014053f --- /dev/null +++ b/packer/service_VRouter/81-configure-ssh.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +# Configures critical settings for OpenSSH server. + +exec 1>&2 +set -o errexit -o nounset -o pipefail +set -x + +gawk -i inplace -f- /etc/ssh/sshd_config <<'EOF' +BEGIN { update = "PasswordAuthentication no" } +/^[#\s]*PasswordAuthentication\s*/ { $0 = update; found = 1 } +{ print } +ENDFILE { if (!found) print update } +EOF + +gawk -i inplace -f- /etc/ssh/sshd_config <<'EOF' +BEGIN { update = "PermitRootLogin without-password" } +/^[#\s]*PermitRootLogin\s*/ { $0 = update; found = 1 } +{ print } +ENDFILE { if (!found) print update } +EOF + +gawk -i inplace -f- /etc/ssh/sshd_config <<'EOF' +BEGIN { update = "UseDNS no" } +/^[#\s]*UseDNS\s*/ { $0 = update; found = 1 } +{ print } +ENDFILE { if (!found) print update } +EOF + +gawk -i inplace -f- /etc/ssh/sshd_config <<'EOF' +BEGIN { update = "AllowTcpForwarding yes" } +/^[#\s]*AllowTcpForwarding\s*/ { $0 = update; found = 1 } +{ print } +ENDFILE { if (!found) print update } +EOF + +gawk -i inplace -f- /etc/ssh/sshd_config <<'EOF' +BEGIN { update = "AllowAgentForwarding yes" } +/^[#\s]*AllowAgentForwarding\s*/ { $0 = update; found = 1 } +{ print } +ENDFILE { if (!found) print update } +EOF + +sync diff --git a/packer/service_VRouter/82-configure-context.sh b/packer/service_VRouter/82-configure-context.sh new file mode 100644 index 00000000..8552eb54 --- /dev/null +++ b/packer/service_VRouter/82-configure-context.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +# Configures and enables service context. + +exec 1>&2 +set -o errexit -o nounset -o pipefail +set -x + +printf '#!/bin/sh\n\ntrue\n' > /etc/one-context.d/loc-12-firewall +printf '#!/bin/sh\n\ntrue\n' > /etc/one-context.d/loc-15-ip_forward +printf '#!/bin/sh\n\ntrue\n' > /etc/one-context.d/loc-15-keepalived + +mv /etc/one-appliance/net-90 /etc/one-context.d/net-90-service-appliance +mv /etc/one-appliance/net-99 /etc/one-context.d/net-99-report-ready + +chown root:root /etc/one-context.d/* +chmod u=rwx,go=rx /etc/one-context.d/* + +sync diff --git a/packer/service_VRouter/VRouter.pkr.hcl b/packer/service_VRouter/VRouter.pkr.hcl new file mode 100644 index 00000000..3d8aa034 --- /dev/null +++ b/packer/service_VRouter/VRouter.pkr.hcl @@ -0,0 +1,103 @@ +source "null" "null" { communicator = "none" } + +build { + sources = ["source.null.null"] + + provisioner "shell-local" { + inline = [ + "mkdir -p ${var.input_dir}/context", + "${var.input_dir}/gen_context > ${var.input_dir}/context/context.sh", + "mkisofs -o ${var.input_dir}/${var.appliance_name}-context.iso -V CONTEXT -J -R ${var.input_dir}/context", + ] + } +} + +# Build VM image +source "qemu" "VRouter" { + cpus = 2 + memory = 2048 + accelerator = "kvm" + + iso_url = "export/alpine318.qcow2" + iso_checksum = "none" + + headless = var.headless + + disk_image = true + disk_cache = "unsafe" + disk_interface = "virtio" + net_device = "virtio-net" + format = "qcow2" + disk_compression = false + disk_size = 2048 + + output_directory = var.output_dir + + qemuargs = [ ["-serial", "stdio"], + ["-cpu", "host"], + ["-cdrom", "${var.input_dir}/${var.appliance_name}-context.iso"], + # MAC addr needs to mach ETH0_MAC from context iso + ["-netdev", "user,id=net0,hostfwd=tcp::{{ .SSHHostPort }}-:22"], + ["-device", "virtio-net-pci,netdev=net0,mac=00:11:22:33:44:55"] + ] + ssh_username = "root" + ssh_password = "opennebula" + ssh_wait_timeout = "900s" + shutdown_command = "poweroff" + vm_name = "${var.appliance_name}" +} + +build { + sources = ["source.qemu.VRouter"] + + # update & revert insecure ssh options done by context start_script + provisioner "shell" { + scripts = [ + "${var.input_dir}/10-update.sh", + "${var.input_dir}/81-configure-ssh.sh" + ] + } + + provisioner "shell" { + inline_shebang = "/bin/bash -e" + inline = [ + "install -o 0 -g 0 -m u=rwx,g=rx,o= -d /etc/one-appliance/{,service.d/,lib/}", + "install -o 0 -g 0 -m u=rwx,g=rx,o=rx -d /opt/one-appliance/{,bin/}" + ] + } + + provisioner "file" { + sources = [ + "appliances/service", + "appliances/scripts/net-90", + "appliances/scripts/net-99", + ] + destination = "/etc/one-appliance/" + } + provisioner "file" { + sources = ["appliances/lib/helpers.rb"] + destination = "/etc/one-appliance/lib/" + } + provisioner "file" { + sources = ["appliances/VRouter"] + destination = "/etc/one-appliance/service.d/" + } + + provisioner "shell" { + scripts = ["${var.input_dir}/82-configure-context.sh"] + } + + provisioner "shell" { + inline_shebang = "/bin/bash -e" + inline = ["/etc/one-appliance/service install"] + } + + post-processor "shell-local" { + execute_command = ["bash", "-c", "{{.Vars}} {{.Script}}"] + environment_vars = [ + "OUTPUT_DIR=${var.output_dir}", + "APPLIANCE_NAME=${var.appliance_name}", + ] + scripts = [ "packer/postprocess.sh" ] + } +} diff --git a/packer/service_VRouter/gen_context b/packer/service_VRouter/gen_context new file mode 100755 index 00000000..18f6985e --- /dev/null +++ b/packer/service_VRouter/gen_context @@ -0,0 +1,30 @@ +#!/bin/bash +SCRIPT=$(cat <> FILENAME } +EOF + +gawk -i inplace -f- /etc/ssh/sshd_config <<'EOF' +BEGIN { update = "PermitRootLogin yes" } +/^[#\s]*PermitRootLogin\s*/ { \$0 = update; found = 1 } +{ print } +END { if (!found) print update >> FILENAME } +EOF + +rc-service sshd reload + +echo "nameserver 1.1.1.1" > /etc/resolv.conf +MAINEND +) + +cat<&2 +set -o errexit -o nounset -o pipefail +set -x + +service haveged stop ||: + apk update apk --no-cache add bash + +sync diff --git a/packer/service_vnf/variables.pkr.hcl b/packer/service_vnf/variables.pkr.hcl index 0bc9ffe2..aea2fd7c 100644 --- a/packer/service_vnf/variables.pkr.hcl +++ b/packer/service_vnf/variables.pkr.hcl @@ -20,4 +20,3 @@ variable "version" { type = string default = "" } - diff --git a/packer/service_vnf/vnf.pkr.hcl b/packer/service_vnf/vnf.pkr.hcl index f8f449b5..d3dce7d1 100644 --- a/packer/service_vnf/vnf.pkr.hcl +++ b/packer/service_vnf/vnf.pkr.hcl @@ -67,42 +67,42 @@ build { provisioner "file" { destination = "/etc/one-appliance/net-90" - source = "appliances/scripts/context_service_net-90.sh" + source = "appliances/legacy/scripts/context_service_net-90.sh" } provisioner "file" { destination = "/etc/one-appliance/net-99" - source = "appliances/scripts/context_service_net-99.sh" + source = "appliances/legacy/scripts/context_service_net-99.sh" } provisioner "file" { destination = "/etc/one-appliance/service" - source = "appliances/service" + source = "appliances/legacy/service" } provisioner "file" { destination = "/etc/one-appliance/service.d/common.sh" - source = "appliances/lib/common.sh" + source = "appliances/legacy/lib/common.sh" } provisioner "file" { destination = "/etc/one-appliance/service.d/functions.sh" - source = "appliances/lib/functions.sh" + source = "appliances/legacy/lib/functions.sh" } provisioner "file" { destination = "/opt/one-appliance/bin/context-helper" - source = "appliances/lib/context-helper.py" + source = "appliances/legacy/lib/context-helper.py" } provisioner "file" { destination = "/etc/one-appliance/service.d/appliance.sh" - source = "appliances/vnf.sh" + source = "appliances/legacy/vnf.sh" } provisioner "file" { destination = "/opt/one-appliance/" - source = "appliances/lib/artifacts/vnf" + source = "appliances/legacy/lib/artifacts/vnf" } provisioner "shell" { diff --git a/packer/service_wordpress/variables.pkr.hcl b/packer/service_wordpress/variables.pkr.hcl index 65735332..c37f371d 100644 --- a/packer/service_wordpress/variables.pkr.hcl +++ b/packer/service_wordpress/variables.pkr.hcl @@ -20,4 +20,3 @@ variable "version" { type = string default = "" } - diff --git a/packer/service_wordpress/wordpress.pkr.hcl b/packer/service_wordpress/wordpress.pkr.hcl index 370621be..888e3c36 100644 --- a/packer/service_wordpress/wordpress.pkr.hcl +++ b/packer/service_wordpress/wordpress.pkr.hcl @@ -65,37 +65,37 @@ build { } provisioner "file" { - source = "appliances/scripts/context_service_net-90.sh" + source = "appliances/legacy/scripts/context_service_net-90.sh" destination = "/etc/one-appliance/net-90" } provisioner "file" { - source = "appliances/scripts/context_service_net-99.sh" + source = "appliances/legacy/scripts/context_service_net-99.sh" destination = "/etc/one-appliance/net-99" } provisioner "file" { - source = "appliances/service" + source = "appliances/legacy/service" destination = "/etc/one-appliance/service" } provisioner "file" { - source = "appliances/lib/common.sh" + source = "appliances/legacy/lib/common.sh" destination = "/etc/one-appliance/service.d/common.sh" } provisioner "file" { - source = "appliances/lib/functions.sh" + source = "appliances/legacy/lib/functions.sh" destination = "/etc/one-appliance/service.d/functions.sh" } provisioner "file" { - source = "appliances/lib/context-helper.py" + source = "appliances/legacy/lib/context-helper.py" destination = "/opt/one-appliance/bin/context-helper" } provisioner "file" { - source = "appliances/wordpress.sh" + source = "appliances/legacy/wordpress.sh" destination = "/etc/one-appliance/service.d/appliance.sh" }