chore: publish pxmon v0.2.0
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type RepoTunnelOptions struct {
|
||||
Gateway string
|
||||
GatewayIP string
|
||||
Table int
|
||||
Priority int
|
||||
PackageManager string
|
||||
Command string
|
||||
KeepEnabled bool
|
||||
NoRule bool
|
||||
}
|
||||
|
||||
type RepoTunnelState struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Proxy string `json:"proxy,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Service) RepoTunnelEnable(ctx context.Context, selector string, opts RepoTunnelOptions) (string, error) {
|
||||
script, err := repoTunnelEnableScript(opts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
out, err := s.RunRemoteShell(ctx, selector, script)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
gw, err := parseRepoTunnelGateway(opts.Gateway)
|
||||
if err == nil {
|
||||
_ = s.updateRepoTunnelState(selector, RepoTunnelState{
|
||||
Enabled: true,
|
||||
Proxy: gw.proxyURL,
|
||||
Source: strings.ToLower(strings.TrimSpace(opts.PackageManager)),
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Service) RepoTunnelDisable(ctx context.Context, selector string, opts RepoTunnelOptions) (string, error) {
|
||||
script, err := repoTunnelDisableScript(opts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
out, err := s.RunRemoteShell(ctx, selector, script)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_ = s.updateRepoTunnelState(selector, RepoTunnelState{})
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Service) RepoTunnelState(ctx context.Context, selector string) (RepoTunnelState, error) {
|
||||
out, err := s.RunRemoteShell(ctx, selector, repoTunnelDetectScript())
|
||||
if err != nil {
|
||||
return RepoTunnelState{}, err
|
||||
}
|
||||
state := RepoTunnelState{}
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
switch {
|
||||
case strings.HasPrefix(line, "proxy="):
|
||||
state.Proxy = strings.TrimSpace(strings.TrimPrefix(line, "proxy="))
|
||||
case strings.HasPrefix(line, "source="):
|
||||
state.Source = strings.TrimSpace(strings.TrimPrefix(line, "source="))
|
||||
}
|
||||
}
|
||||
state.Enabled = state.Proxy != ""
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func (s *Service) updateRepoTunnelState(selector string, state RepoTunnelState) error {
|
||||
reg, err := s.store.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c, idx, err := findCluster(reg, selector)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.RepoTunnel = state
|
||||
c.UpdatedAt = s.now().UTC()
|
||||
reg.Clusters[idx] = c
|
||||
return s.store.Save(reg)
|
||||
}
|
||||
|
||||
func (s *Service) RepoTunnelStatus(ctx context.Context, selector string) (string, error) {
|
||||
return s.RunRemoteShell(ctx, selector, repoTunnelStatusScript())
|
||||
}
|
||||
|
||||
func (s *Service) RepoTunnelInstall(ctx context.Context, selector string, opts RepoTunnelOptions) (string, error) {
|
||||
if strings.TrimSpace(opts.Command) == "" {
|
||||
return "", errors.New("install command is required")
|
||||
}
|
||||
enableScript, err := repoTunnelEnableScript(opts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
disableScript, err := repoTunnelDisableScript(opts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
body := enableScript + "\n" + strings.TrimSpace(opts.Command) + "\n"
|
||||
if !opts.KeepEnabled {
|
||||
body = enableScript + "\ncleanup_pxmon_repo_tunnel() {\n" + disableScript + "\n}\ntrap cleanup_pxmon_repo_tunnel EXIT\n" + strings.TrimSpace(opts.Command) + "\n"
|
||||
}
|
||||
return s.RunRemoteShell(ctx, selector, body)
|
||||
}
|
||||
|
||||
func RepoTunnelGatewayScript(port int, allowCIDRs []string) (string, error) {
|
||||
if port == 0 {
|
||||
port = 3128
|
||||
}
|
||||
if port < 1 || port > 65535 {
|
||||
return "", errors.New("--port must be in range 1..65535")
|
||||
}
|
||||
if len(allowCIDRs) == 0 {
|
||||
return "", errors.New("at least one --allow CIDR/IP is required")
|
||||
}
|
||||
aclParts := make([]string, 0, len(allowCIDRs))
|
||||
for _, raw := range allowCIDRs {
|
||||
v := strings.TrimSpace(raw)
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
if !validSquidSrcACL(v) {
|
||||
return "", fmt.Errorf("invalid --allow %q; use an IP or CIDR without spaces", raw)
|
||||
}
|
||||
aclParts = append(aclParts, v)
|
||||
}
|
||||
if len(aclParts) == 0 {
|
||||
return "", errors.New("at least one --allow CIDR/IP is required")
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`set -eu
|
||||
if command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y squid
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
yum install -y squid
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y squid
|
||||
else
|
||||
echo "no supported package manager found for squid install" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
conf=/etc/squid/squid.conf
|
||||
cp -a "$conf" "$conf.pxmon-bak.$(date +%%Y%%m%%d%%H%%M%%S)"
|
||||
awk '
|
||||
/# BEGIN PXMON REPO TUNNEL/ {skip=1; next}
|
||||
/# END PXMON REPO TUNNEL/ {skip=0; next}
|
||||
!skip {print}
|
||||
' "$conf" > "$conf.tmp"
|
||||
mv "$conf.tmp" "$conf"
|
||||
|
||||
block=$(mktemp)
|
||||
{
|
||||
echo "# BEGIN PXMON REPO TUNNEL"
|
||||
if ! grep -Eq "^http_port[[:space:]]+([^[:space:]]+:)?%d\b" "$conf"; then
|
||||
echo "http_port %d"
|
||||
fi
|
||||
echo "acl pxmon_repo_tunnel src %s"
|
||||
echo "http_access allow pxmon_repo_tunnel"
|
||||
echo "# END PXMON REPO TUNNEL"
|
||||
} > "$block"
|
||||
|
||||
if grep -q "^http_access deny all" "$conf"; then
|
||||
awk -v block="$block" '
|
||||
BEGIN {while ((getline line < block) > 0) b = b line "\n"; close(block); inserted=0}
|
||||
/^http_access deny all/ && !inserted {printf "%%s", b; inserted=1}
|
||||
{print}
|
||||
END {if (!inserted) printf "%%s", b}
|
||||
' "$conf" > "$conf.tmp"
|
||||
mv "$conf.tmp" "$conf"
|
||||
else
|
||||
cat "$block" >> "$conf"
|
||||
fi
|
||||
rm -f "$block"
|
||||
|
||||
systemctl enable --now squid
|
||||
systemctl restart squid
|
||||
if command -v firewall-cmd >/dev/null 2>&1 && systemctl is-active --quiet firewalld; then
|
||||
firewall-cmd --add-port=%d/tcp --permanent
|
||||
firewall-cmd --reload
|
||||
fi
|
||||
echo "pxmon repo gateway ready on port %d"
|
||||
`, port, port, strings.Join(aclParts, " "), port, port), nil
|
||||
}
|
||||
|
||||
func validSquidSrcACL(v string) bool {
|
||||
for _, r := range v {
|
||||
if r >= 'a' && r <= 'z' {
|
||||
continue
|
||||
}
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
continue
|
||||
}
|
||||
if r >= '0' && r <= '9' {
|
||||
continue
|
||||
}
|
||||
switch r {
|
||||
case '.', ':', '/', '_', '-':
|
||||
continue
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return v != ""
|
||||
}
|
||||
|
||||
func repoTunnelEnableScript(opts RepoTunnelOptions) (string, error) {
|
||||
gw, err := parseRepoTunnelGateway(opts.Gateway)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !opts.NoRule && opts.Table <= 0 {
|
||||
return "", errors.New("--table is required unless --no-rule is used")
|
||||
}
|
||||
manager := strings.ToLower(strings.TrimSpace(opts.PackageManager))
|
||||
if manager == "" {
|
||||
manager = "auto"
|
||||
}
|
||||
if manager != "auto" && manager != "apt" && manager != "dnf" && manager != "yum" {
|
||||
return "", fmt.Errorf("unsupported --manager %q", opts.PackageManager)
|
||||
}
|
||||
ruleLine := ""
|
||||
if !opts.NoRule {
|
||||
addCmd := fmt.Sprintf("ip rule add to \"$PXMON_GATEWAY_IP/32\" table %d", opts.Table)
|
||||
if opts.Priority > 0 {
|
||||
addCmd = fmt.Sprintf("ip rule add priority %d to \"$PXMON_GATEWAY_IP/32\" table %d", opts.Priority, opts.Table)
|
||||
}
|
||||
ruleLine = fmt.Sprintf(`
|
||||
if ! ip rule show | grep -Eq "to[[:space:]]+$PXMON_GATEWAY_IP(/32)?[[:space:]].*lookup[[:space:]]+%d\b"; then
|
||||
%s
|
||||
fi`, opts.Table, addCmd)
|
||||
}
|
||||
return fmt.Sprintf(`set -eu
|
||||
PXMON_GATEWAY_HOST=%s
|
||||
PXMON_GATEWAY_IP=%s
|
||||
PXMON_PROXY_URL=%s
|
||||
PXMON_MANAGER=%s
|
||||
if [ -z "$PXMON_GATEWAY_IP" ]; then
|
||||
PXMON_GATEWAY_IP=$(getent ahostsv4 "$PXMON_GATEWAY_HOST" | awk '{print $1; exit}')
|
||||
fi
|
||||
if [ -z "$PXMON_GATEWAY_IP" ]; then
|
||||
echo "cannot resolve repo gateway: $PXMON_GATEWAY_HOST" >&2
|
||||
exit 1
|
||||
fi
|
||||
%s
|
||||
pxmon_repo_manager="$PXMON_MANAGER"
|
||||
if [ "$pxmon_repo_manager" = "auto" ]; then
|
||||
if command -v apt-get >/dev/null 2>&1; then pxmon_repo_manager=apt
|
||||
elif command -v dnf >/dev/null 2>&1; then pxmon_repo_manager=dnf
|
||||
elif command -v yum >/dev/null 2>&1; then pxmon_repo_manager=yum
|
||||
else echo "no supported package manager found" >&2; exit 1
|
||||
fi
|
||||
fi
|
||||
case "$pxmon_repo_manager" in
|
||||
apt)
|
||||
mkdir -p /etc/apt/apt.conf.d
|
||||
cat > /etc/apt/apt.conf.d/99-pxmon-repo-tunnel <<EOF
|
||||
Acquire::http::Proxy "$PXMON_PROXY_URL";
|
||||
Acquire::https::Proxy "$PXMON_PROXY_URL";
|
||||
EOF
|
||||
;;
|
||||
dnf|yum)
|
||||
conf=/etc/dnf/dnf.conf
|
||||
[ "$pxmon_repo_manager" = "yum" ] && conf=/etc/yum.conf
|
||||
[ -f "$conf" ] || touch "$conf"
|
||||
awk '
|
||||
/# BEGIN PXMON REPO TUNNEL/ {skip=1; next}
|
||||
/# END PXMON REPO TUNNEL/ {skip=0; next}
|
||||
!skip {print}
|
||||
' "$conf" > "$conf.tmp"
|
||||
mv "$conf.tmp" "$conf"
|
||||
{
|
||||
echo "# BEGIN PXMON REPO TUNNEL"
|
||||
echo "proxy=$PXMON_PROXY_URL"
|
||||
echo "# END PXMON REPO TUNNEL"
|
||||
} >> "$conf"
|
||||
;;
|
||||
esac
|
||||
echo "pxmon repo tunnel enabled: proxy=$PXMON_PROXY_URL gateway_ip=$PXMON_GATEWAY_IP manager=$pxmon_repo_manager"
|
||||
`, shellQuote(gw.host), shellQuote(strings.TrimSpace(opts.GatewayIP)), shellQuote(gw.proxyURL), shellQuote(manager), ruleLine), nil
|
||||
}
|
||||
|
||||
func repoTunnelDisableScript(opts RepoTunnelOptions) (string, error) {
|
||||
gw, err := parseRepoTunnelGateway(opts.Gateway)
|
||||
if err != nil && !opts.NoRule {
|
||||
return "", err
|
||||
}
|
||||
ruleLine := ""
|
||||
if !opts.NoRule {
|
||||
if opts.Table <= 0 {
|
||||
return "", errors.New("--table is required unless --no-rule is used")
|
||||
}
|
||||
ruleLine = fmt.Sprintf(`
|
||||
PXMON_GATEWAY_HOST=%s
|
||||
PXMON_GATEWAY_IP=%s
|
||||
if [ -z "$PXMON_GATEWAY_IP" ]; then
|
||||
PXMON_GATEWAY_IP=$(getent ahostsv4 "$PXMON_GATEWAY_HOST" | awk '{print $1; exit}')
|
||||
fi
|
||||
if [ -n "$PXMON_GATEWAY_IP" ]; then
|
||||
while ip rule show | grep -Eq "to[[:space:]]+$PXMON_GATEWAY_IP(/32)?[[:space:]].*lookup[[:space:]]+%d\b"; do
|
||||
ip rule del to "$PXMON_GATEWAY_IP/32" table %d 2>/dev/null || break
|
||||
done
|
||||
fi`, shellQuote(gw.host), shellQuote(strings.TrimSpace(opts.GatewayIP)), opts.Table, opts.Table)
|
||||
}
|
||||
return fmt.Sprintf(`set -eu
|
||||
rm -f /etc/apt/apt.conf.d/99-pxmon-repo-tunnel
|
||||
for conf in /etc/dnf/dnf.conf /etc/yum.conf; do
|
||||
if [ -f "$conf" ]; then
|
||||
awk '
|
||||
/# BEGIN PXMON REPO TUNNEL/ {skip=1; next}
|
||||
/# END PXMON REPO TUNNEL/ {skip=0; next}
|
||||
!skip {print}
|
||||
' "$conf" > "$conf.tmp"
|
||||
mv "$conf.tmp" "$conf"
|
||||
fi
|
||||
done
|
||||
%s
|
||||
echo "pxmon repo tunnel disabled"
|
||||
`, ruleLine), nil
|
||||
}
|
||||
|
||||
func repoTunnelStatusScript() string {
|
||||
return `set -eu
|
||||
echo "== ip rules =="
|
||||
ip rule show | grep -E "lookup|table" || true
|
||||
echo
|
||||
echo "== apt proxy =="
|
||||
[ -f /etc/apt/apt.conf.d/99-pxmon-repo-tunnel ] && cat /etc/apt/apt.conf.d/99-pxmon-repo-tunnel || echo "(none)"
|
||||
echo
|
||||
echo "== dnf/yum proxy =="
|
||||
for conf in /etc/dnf/dnf.conf /etc/yum.conf; do
|
||||
[ -f "$conf" ] || continue
|
||||
echo "-- $conf"
|
||||
awk '/# BEGIN PXMON REPO TUNNEL/,/# END PXMON REPO TUNNEL/ {print}' "$conf"
|
||||
done`
|
||||
}
|
||||
|
||||
func repoTunnelDetectScript() string {
|
||||
return `set -eu
|
||||
if [ -f /etc/apt/apt.conf.d/99-pxmon-repo-tunnel ]; then
|
||||
proxy=$(sed -n 's/.*Proxy[[:space:]]*"\([^"]*\)".*/\1/p' /etc/apt/apt.conf.d/99-pxmon-repo-tunnel | head -1)
|
||||
[ -n "$proxy" ] && printf 'proxy=%s\nsource=apt\n' "$proxy" && exit 0
|
||||
fi
|
||||
for conf in /etc/dnf/dnf.conf /etc/yum.conf; do
|
||||
[ -f "$conf" ] || continue
|
||||
proxy=$(awk '
|
||||
/# BEGIN PXMON REPO TUNNEL/ {inside=1; next}
|
||||
/# END PXMON REPO TUNNEL/ {inside=0; next}
|
||||
inside && /^proxy[[:space:]]*=/ {
|
||||
sub(/^[^=]*=/, "")
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "")
|
||||
print
|
||||
exit
|
||||
}
|
||||
' "$conf")
|
||||
[ -n "$proxy" ] && printf 'proxy=%s\nsource=%s\n' "$proxy" "$conf" && exit 0
|
||||
done
|
||||
exit 0`
|
||||
}
|
||||
|
||||
type repoTunnelGateway struct {
|
||||
host string
|
||||
port int
|
||||
proxyURL string
|
||||
}
|
||||
|
||||
func parseRepoTunnelGateway(raw string) (repoTunnelGateway, error) {
|
||||
v := strings.TrimSpace(raw)
|
||||
if v == "" {
|
||||
return repoTunnelGateway{}, errors.New("--gateway is required")
|
||||
}
|
||||
if strings.HasPrefix(v, "http://") {
|
||||
v = strings.TrimPrefix(v, "http://")
|
||||
}
|
||||
if strings.HasPrefix(v, "https://") {
|
||||
return repoTunnelGateway{}, errors.New("--gateway must be an http proxy endpoint, not https")
|
||||
}
|
||||
host, portRaw, err := net.SplitHostPort(v)
|
||||
if err != nil {
|
||||
if strings.Count(v, ":") > 1 {
|
||||
return repoTunnelGateway{}, fmt.Errorf("invalid --gateway %q; use host:port or [ipv6]:port", raw)
|
||||
}
|
||||
host = v
|
||||
portRaw = "3128"
|
||||
}
|
||||
host = strings.Trim(host, "[]")
|
||||
if strings.TrimSpace(host) == "" {
|
||||
return repoTunnelGateway{}, errors.New("--gateway host is empty")
|
||||
}
|
||||
port, err := strconv.Atoi(portRaw)
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
return repoTunnelGateway{}, fmt.Errorf("invalid --gateway port %q", portRaw)
|
||||
}
|
||||
return repoTunnelGateway{
|
||||
host: host,
|
||||
port: port,
|
||||
proxyURL: "http://" + net.JoinHostPort(host, strconv.Itoa(port)),
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user