kitsune-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
 - data/.rspec +3 -0
 - data/CHANGELOG.md +6 -0
 - data/CODE_OF_CONDUCT.md +132 -0
 - data/LICENSE.txt +21 -0
 - data/README.md +192 -0
 - data/Rakefile +8 -0
 - data/bin/kit +8 -0
 - data/kitsune-kit-logo.jpg +0 -0
 - data/lib/kitsune/blueprints/.env.template +17 -0
 - data/lib/kitsune/blueprints/docker/postgres.yml +20 -0
 - data/lib/kitsune/blueprints/kit.env.template +1 -0
 - data/lib/kitsune/kit/cli.rb +64 -0
 - data/lib/kitsune/kit/commands/bootstrap.rb +118 -0
 - data/lib/kitsune/kit/commands/bootstrap_docker.rb +66 -0
 - data/lib/kitsune/kit/commands/init.rb +148 -0
 - data/lib/kitsune/kit/commands/install_docker_engine.rb +146 -0
 - data/lib/kitsune/kit/commands/postinstall_docker.rb +142 -0
 - data/lib/kitsune/kit/commands/provision.rb +43 -0
 - data/lib/kitsune/kit/commands/setup_docker_prereqs.rb +150 -0
 - data/lib/kitsune/kit/commands/setup_firewall.rb +132 -0
 - data/lib/kitsune/kit/commands/setup_postgres_docker.rb +246 -0
 - data/lib/kitsune/kit/commands/setup_unattended.rb +132 -0
 - data/lib/kitsune/kit/commands/setup_user.rb +189 -0
 - data/lib/kitsune/kit/commands/switch_env.rb +42 -0
 - data/lib/kitsune/kit/defaults.rb +56 -0
 - data/lib/kitsune/kit/env_loader.rb +41 -0
 - data/lib/kitsune/kit/options_builder.rb +26 -0
 - data/lib/kitsune/kit/provisioner.rb +109 -0
 - data/lib/kitsune/kit/version.rb +7 -0
 - data/lib/kitsune/kit.rb +10 -0
 - data/sig/kitsune/kit.rbs +6 -0
 - metadata +180 -0
 
| 
         @@ -0,0 +1,132 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require "thor"
         
     | 
| 
      
 2 
     | 
    
         
            +
            require "net/ssh"
         
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative "../defaults"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require_relative "../options_builder"
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            module Kitsune
         
     | 
| 
      
 7 
     | 
    
         
            +
              module Kit
         
     | 
| 
      
 8 
     | 
    
         
            +
                module Commands
         
     | 
| 
      
 9 
     | 
    
         
            +
                  class SetupFirewall < Thor
         
     | 
| 
      
 10 
     | 
    
         
            +
                    namespace "setup_firewall"
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                    class_option :server_ip,    aliases: "-s", required: true, desc: "Server IP address or hostname"
         
     | 
| 
      
 13 
     | 
    
         
            +
                    class_option :ssh_port,     aliases: "-p", desc: "SSH port"
         
     | 
| 
      
 14 
     | 
    
         
            +
                    class_option :ssh_key_path, aliases: "-k", desc: "Path to your private SSH key"
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                    desc "create", "Setup UFW firewall rules on the remote server"
         
     | 
| 
      
 17 
     | 
    
         
            +
                    def create
         
     | 
| 
      
 18 
     | 
    
         
            +
                      filled_options = Kitsune::Kit::OptionsBuilder.build(
         
     | 
| 
      
 19 
     | 
    
         
            +
                        options,
         
     | 
| 
      
 20 
     | 
    
         
            +
                        required: [:server_ip],
         
     | 
| 
      
 21 
     | 
    
         
            +
                        defaults: Kitsune::Kit::Defaults.ssh
         
     | 
| 
      
 22 
     | 
    
         
            +
                      )
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                      with_ssh_connection(filled_options) do |ssh|
         
     | 
| 
      
 25 
     | 
    
         
            +
                        perform_setup(ssh, filled_options)
         
     | 
| 
      
 26 
     | 
    
         
            +
                      end
         
     | 
| 
      
 27 
     | 
    
         
            +
                    end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                    desc "rollback", "Remove UFW firewall rules and disable UFW on the remote server"
         
     | 
| 
      
 30 
     | 
    
         
            +
                    def rollback
         
     | 
| 
      
 31 
     | 
    
         
            +
                      filled_options = Kitsune::Kit::OptionsBuilder.build(
         
     | 
| 
      
 32 
     | 
    
         
            +
                        options,
         
     | 
| 
      
 33 
     | 
    
         
            +
                        required: [:server_ip],
         
     | 
| 
      
 34 
     | 
    
         
            +
                        defaults: Kitsune::Kit::Defaults.ssh
         
     | 
| 
      
 35 
     | 
    
         
            +
                      )
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                      with_ssh_connection(filled_options) do |ssh|
         
     | 
| 
      
 38 
     | 
    
         
            +
                        perform_rollback(ssh, filled_options)
         
     | 
| 
      
 39 
     | 
    
         
            +
                      end
         
     | 
| 
      
 40 
     | 
    
         
            +
                    end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                    no_commands do
         
     | 
| 
      
 43 
     | 
    
         
            +
                      def with_ssh_connection(filled_options)
         
     | 
| 
      
 44 
     | 
    
         
            +
                        server = filled_options[:server_ip]
         
     | 
| 
      
 45 
     | 
    
         
            +
                        port   = filled_options[:ssh_port]
         
     | 
| 
      
 46 
     | 
    
         
            +
                        key    = File.expand_path(filled_options[:ssh_key_path])
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                        say "🔑 Connecting as deploy@#{server}:#{port}", :green
         
     | 
| 
      
 49 
     | 
    
         
            +
                        Net::SSH.start(server, "deploy", port: port, keys: [key], non_interactive: true, timeout: 5) do |ssh|
         
     | 
| 
      
 50 
     | 
    
         
            +
                          yield ssh
         
     | 
| 
      
 51 
     | 
    
         
            +
                        end
         
     | 
| 
      
 52 
     | 
    
         
            +
                      end
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                      def perform_setup(ssh, filled_options)
         
     | 
| 
      
 55 
     | 
    
         
            +
                        ssh_port = filled_options[:ssh_port]
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                        output = ssh.exec! <<~EOH
         
     | 
| 
      
 58 
     | 
    
         
            +
                          set -e
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                          echo "✍🏻 Updating repositories and ensuring UFW is installed…"
         
     | 
| 
      
 61 
     | 
    
         
            +
                          if ! dpkg -l | grep -q ufw; then
         
     | 
| 
      
 62 
     | 
    
         
            +
                            sudo apt-get update -y
         
     | 
| 
      
 63 
     | 
    
         
            +
                            sudo apt-get install -y ufw && echo "   - ufw installed"
         
     | 
| 
      
 64 
     | 
    
         
            +
                          else
         
     | 
| 
      
 65 
     | 
    
         
            +
                            echo "   - ufw is already installed"
         
     | 
| 
      
 66 
     | 
    
         
            +
                          fi
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                          echo "✍🏻 Configuring UFW rules…"
         
     | 
| 
      
 69 
     | 
    
         
            +
                          add_rule() {
         
     | 
| 
      
 70 
     | 
    
         
            +
                            local rule="$1"
         
     | 
| 
      
 71 
     | 
    
         
            +
                            if ! sudo ufw status | grep -q "$rule"; then
         
     | 
| 
      
 72 
     | 
    
         
            +
                              sudo ufw allow "$rule" >/dev/null 2>&1 && echo "   - rule '$rule' added"
         
     | 
| 
      
 73 
     | 
    
         
            +
                            else
         
     | 
| 
      
 74 
     | 
    
         
            +
                              echo "   - rule '$rule' already exists"
         
     | 
| 
      
 75 
     | 
    
         
            +
                            fi
         
     | 
| 
      
 76 
     | 
    
         
            +
                          }
         
     | 
| 
      
 77 
     | 
    
         
            +
                          add_rule "#{ssh_port}/tcp"
         
     | 
| 
      
 78 
     | 
    
         
            +
                          add_rule "80/tcp"
         
     | 
| 
      
 79 
     | 
    
         
            +
                          add_rule "443/tcp"
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                          echo "✍🏻 Enabling UFW logging…"
         
     | 
| 
      
 82 
     | 
    
         
            +
                          if ! sudo ufw status verbose | grep -q "Logging: on"; then
         
     | 
| 
      
 83 
     | 
    
         
            +
                            sudo ufw logging on >/dev/null 2>&1 && echo "   - logging enabled"
         
     | 
| 
      
 84 
     | 
    
         
            +
                          else
         
     | 
| 
      
 85 
     | 
    
         
            +
                            echo "   - logging was already enabled"
         
     | 
| 
      
 86 
     | 
    
         
            +
                          fi
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
                          echo "✍🏻 Enabling UFW…"
         
     | 
| 
      
 89 
     | 
    
         
            +
                          if sudo ufw status | grep -q "Status: inactive"; then
         
     | 
| 
      
 90 
     | 
    
         
            +
                            sudo ufw --force enable >/dev/null 2>&1 && echo "   - UFW enabled"
         
     | 
| 
      
 91 
     | 
    
         
            +
                          else
         
     | 
| 
      
 92 
     | 
    
         
            +
                            echo "   - UFW is already enabled"
         
     | 
| 
      
 93 
     | 
    
         
            +
                          fi
         
     | 
| 
      
 94 
     | 
    
         
            +
                        EOH
         
     | 
| 
      
 95 
     | 
    
         
            +
                        say output
         
     | 
| 
      
 96 
     | 
    
         
            +
                        say "✅ Firewall setup completed", :green
         
     | 
| 
      
 97 
     | 
    
         
            +
                      end
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
                      def perform_rollback(ssh, filled_options)
         
     | 
| 
      
 100 
     | 
    
         
            +
                        ssh_port = filled_options[:ssh_port]
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                        output = ssh.exec! <<~EOH
         
     | 
| 
      
 103 
     | 
    
         
            +
                          set -e
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
      
 105 
     | 
    
         
            +
                          echo "🔁 Removing UFW rules…"
         
     | 
| 
      
 106 
     | 
    
         
            +
                          delete_rule() {
         
     | 
| 
      
 107 
     | 
    
         
            +
                            local rule="$1"
         
     | 
| 
      
 108 
     | 
    
         
            +
                            if sudo ufw status | grep -q "$rule"; then
         
     | 
| 
      
 109 
     | 
    
         
            +
                              sudo ufw delete allow "$rule" >/dev/null 2>&1 && echo "   - rule '$rule' removed"
         
     | 
| 
      
 110 
     | 
    
         
            +
                            else
         
     | 
| 
      
 111 
     | 
    
         
            +
                              echo "   - rule '$rule' does not exist"
         
     | 
| 
      
 112 
     | 
    
         
            +
                            fi
         
     | 
| 
      
 113 
     | 
    
         
            +
                          }
         
     | 
| 
      
 114 
     | 
    
         
            +
                          delete_rule "#{ssh_port}/tcp"
         
     | 
| 
      
 115 
     | 
    
         
            +
                          delete_rule "80/tcp"
         
     | 
| 
      
 116 
     | 
    
         
            +
                          delete_rule "443/tcp"
         
     | 
| 
      
 117 
     | 
    
         
            +
             
     | 
| 
      
 118 
     | 
    
         
            +
                          echo "✍🏻 Disabling UFW if active…"
         
     | 
| 
      
 119 
     | 
    
         
            +
                          if sudo ufw status | grep -q "Status: inactive"; then
         
     | 
| 
      
 120 
     | 
    
         
            +
                            echo "   - UFW is already inactive"
         
     | 
| 
      
 121 
     | 
    
         
            +
                          else
         
     | 
| 
      
 122 
     | 
    
         
            +
                            sudo ufw --force disable >/dev/null 2>&1 && echo "   - UFW disabled"
         
     | 
| 
      
 123 
     | 
    
         
            +
                          fi
         
     | 
| 
      
 124 
     | 
    
         
            +
                        EOH
         
     | 
| 
      
 125 
     | 
    
         
            +
                        say output
         
     | 
| 
      
 126 
     | 
    
         
            +
                        say "✅ Firewall rollback completed", :green
         
     | 
| 
      
 127 
     | 
    
         
            +
                      end
         
     | 
| 
      
 128 
     | 
    
         
            +
                    end
         
     | 
| 
      
 129 
     | 
    
         
            +
                  end
         
     | 
| 
      
 130 
     | 
    
         
            +
                end
         
     | 
| 
      
 131 
     | 
    
         
            +
              end
         
     | 
| 
      
 132 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,246 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require "thor"
         
     | 
| 
      
 2 
     | 
    
         
            +
            require "net/ssh"
         
     | 
| 
      
 3 
     | 
    
         
            +
            require "tempfile"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require "fileutils"
         
     | 
| 
      
 5 
     | 
    
         
            +
            require "shellwords"
         
     | 
| 
      
 6 
     | 
    
         
            +
            require_relative "../defaults"
         
     | 
| 
      
 7 
     | 
    
         
            +
            require_relative "../options_builder"
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            module Kitsune
         
     | 
| 
      
 10 
     | 
    
         
            +
              module Kit
         
     | 
| 
      
 11 
     | 
    
         
            +
                module Commands
         
     | 
| 
      
 12 
     | 
    
         
            +
                  class SetupPostgresDocker < Thor
         
     | 
| 
      
 13 
     | 
    
         
            +
                    namespace "setup_postgres_docker"
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                    class_option :server_ip, aliases: "-s", required: true, desc: "Server IP address or hostname"
         
     | 
| 
      
 16 
     | 
    
         
            +
                    class_option :ssh_port, aliases: "-p", desc: "SSH port"
         
     | 
| 
      
 17 
     | 
    
         
            +
                    class_option :ssh_key_path, aliases: "-k", desc: "Path to SSH private key"
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                    desc "create", "Setup PostgreSQL using Docker Compose on remote server"
         
     | 
| 
      
 20 
     | 
    
         
            +
                    def create
         
     | 
| 
      
 21 
     | 
    
         
            +
                      postgres_defaults = Kitsune::Kit::Defaults.postgres
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                      if postgres_defaults[:postgres_password] == "secret"
         
     | 
| 
      
 24 
     | 
    
         
            +
                        say "⚠️ Warning: You are using the default PostgreSQL password ('secret').", :yellow
         
     | 
| 
      
 25 
     | 
    
         
            +
                        if ENV.fetch("KIT_ENV", "development") == "production"
         
     | 
| 
      
 26 
     | 
    
         
            +
                          abort "❌ Production environment requires a secure PostgreSQL password!"
         
     | 
| 
      
 27 
     | 
    
         
            +
                        else
         
     | 
| 
      
 28 
     | 
    
         
            +
                          say "🔒 Please change POSTGRES_PASSWORD in your .env if needed.", :yellow
         
     | 
| 
      
 29 
     | 
    
         
            +
                        end
         
     | 
| 
      
 30 
     | 
    
         
            +
                      end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                      filled_options = Kitsune::Kit::OptionsBuilder.build(
         
     | 
| 
      
 33 
     | 
    
         
            +
                        options,
         
     | 
| 
      
 34 
     | 
    
         
            +
                        required: [:server_ip],
         
     | 
| 
      
 35 
     | 
    
         
            +
                        defaults: Kitsune::Kit::Defaults.ssh
         
     | 
| 
      
 36 
     | 
    
         
            +
                      )
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                      with_ssh_connection(filled_options) do |ssh|
         
     | 
| 
      
 39 
     | 
    
         
            +
                        perform_setup(ssh, postgres_defaults)
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                        database_url = build_database_url(filled_options, postgres_defaults)
         
     | 
| 
      
 42 
     | 
    
         
            +
                        say "🔗 Your DATABASE_URL is:\t", :cyan
         
     | 
| 
      
 43 
     | 
    
         
            +
                        say database_url, :green
         
     | 
| 
      
 44 
     | 
    
         
            +
                      end
         
     | 
| 
      
 45 
     | 
    
         
            +
                    end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                    desc "rollback", "Remove PostgreSQL Docker setup from remote server"
         
     | 
| 
      
 48 
     | 
    
         
            +
                    def rollback
         
     | 
| 
      
 49 
     | 
    
         
            +
                      filled_options = Kitsune::Kit::OptionsBuilder.build(
         
     | 
| 
      
 50 
     | 
    
         
            +
                        options,
         
     | 
| 
      
 51 
     | 
    
         
            +
                        required: [:server_ip],
         
     | 
| 
      
 52 
     | 
    
         
            +
                        defaults: Kitsune::Kit::Defaults.ssh
         
     | 
| 
      
 53 
     | 
    
         
            +
                      )
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                      with_ssh_connection(filled_options) do |ssh|
         
     | 
| 
      
 56 
     | 
    
         
            +
                        perform_rollback(ssh)
         
     | 
| 
      
 57 
     | 
    
         
            +
                      end
         
     | 
| 
      
 58 
     | 
    
         
            +
                    end
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                    no_commands do
         
     | 
| 
      
 61 
     | 
    
         
            +
                      def with_ssh_connection(filled_options)
         
     | 
| 
      
 62 
     | 
    
         
            +
                        server = filled_options[:server_ip]
         
     | 
| 
      
 63 
     | 
    
         
            +
                        port   = filled_options[:ssh_port]
         
     | 
| 
      
 64 
     | 
    
         
            +
                        key    = File.expand_path(filled_options[:ssh_key_path])
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                        say "🔑 Connecting as deploy@#{server}:#{port}", :green
         
     | 
| 
      
 67 
     | 
    
         
            +
                        Net::SSH.start(server, "deploy", port: port, keys: [key], non_interactive: true, timeout: 5) do |ssh|
         
     | 
| 
      
 68 
     | 
    
         
            +
                          yield ssh
         
     | 
| 
      
 69 
     | 
    
         
            +
                        end
         
     | 
| 
      
 70 
     | 
    
         
            +
                      end
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                      def perform_setup(ssh, postgres_defaults)
         
     | 
| 
      
 73 
     | 
    
         
            +
                        docker_compose_local = ".kitsune/docker/postgres.yml"
         
     | 
| 
      
 74 
     | 
    
         
            +
                        unless File.exist?(docker_compose_local)
         
     | 
| 
      
 75 
     | 
    
         
            +
                          say "❌ Docker compose file not found at #{docker_compose_local}.", :red
         
     | 
| 
      
 76 
     | 
    
         
            +
                          exit(1)
         
     | 
| 
      
 77 
     | 
    
         
            +
                        end
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                        docker_dir_remote = "$HOME/docker/postgres"
         
     | 
| 
      
 80 
     | 
    
         
            +
                        docker_compose_remote = "#{docker_dir_remote}/docker-compose.yml"
         
     | 
| 
      
 81 
     | 
    
         
            +
                        docker_env_remote = "#{docker_dir_remote}/.env"
         
     | 
| 
      
 82 
     | 
    
         
            +
                        backup_marker = "/usr/local/backups/setup_postgres_docker.after"
         
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
                        # 1. Create base directory securely
         
     | 
| 
      
 85 
     | 
    
         
            +
                        ssh.exec!("mkdir -p #{docker_dir_remote}")
         
     | 
| 
      
 86 
     | 
    
         
            +
                        ssh.exec!("chmod 700 #{docker_dir_remote}")
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
                        # 2. Upload docker-compose.yml
         
     | 
| 
      
 89 
     | 
    
         
            +
                        say "📦 Uploading docker-compose.yml to remote server...", :cyan
         
     | 
| 
      
 90 
     | 
    
         
            +
                        content_compose = File.read(docker_compose_local)
         
     | 
| 
      
 91 
     | 
    
         
            +
                        upload_file(ssh, content_compose, docker_compose_remote)
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                        # 3. Create .env file for docker-compose based on postgres_defaults
         
     | 
| 
      
 94 
     | 
    
         
            +
                        say "📦 Creating .env file for Docker Compose...", :cyan
         
     | 
| 
      
 95 
     | 
    
         
            +
                        env_content = <<~ENVFILE
         
     | 
| 
      
 96 
     | 
    
         
            +
                          POSTGRES_DB=#{postgres_defaults[:postgres_db]}
         
     | 
| 
      
 97 
     | 
    
         
            +
                          POSTGRES_USER=#{postgres_defaults[:postgres_user]}
         
     | 
| 
      
 98 
     | 
    
         
            +
                          POSTGRES_PASSWORD=#{postgres_defaults[:postgres_password]}
         
     | 
| 
      
 99 
     | 
    
         
            +
                          POSTGRES_PORT=#{postgres_defaults[:postgres_port]}
         
     | 
| 
      
 100 
     | 
    
         
            +
                          POSTGRES_IMAGE=#{postgres_defaults[:postgres_image]}
         
     | 
| 
      
 101 
     | 
    
         
            +
                        ENVFILE
         
     | 
| 
      
 102 
     | 
    
         
            +
                        upload_file(ssh, env_content, docker_env_remote)
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
                        # 4. Secure file permissions
         
     | 
| 
      
 105 
     | 
    
         
            +
                        ssh.exec!("chmod 600 #{docker_compose_remote} #{docker_env_remote}")
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
                        # 5. Create backup marker
         
     | 
| 
      
 108 
     | 
    
         
            +
                        ssh.exec!("sudo mkdir -p /usr/local/backups && sudo touch #{backup_marker}")
         
     | 
| 
      
 109 
     | 
    
         
            +
             
     | 
| 
      
 110 
     | 
    
         
            +
                        # 6. Validate docker-compose.yml
         
     | 
| 
      
 111 
     | 
    
         
            +
                        say "🔍 Validating docker-compose.yml...", :cyan
         
     | 
| 
      
 112 
     | 
    
         
            +
                        validation_output = ssh.exec!("cd #{docker_dir_remote} && docker compose config")
         
     | 
| 
      
 113 
     | 
    
         
            +
                        say validation_output, :cyan
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
                        # 7. Check if container is running
         
     | 
| 
      
 116 
     | 
    
         
            +
                        container_status = ssh.exec!("docker ps --filter 'name=postgres' --format '{{.Status}}'").strip
         
     | 
| 
      
 117 
     | 
    
         
            +
             
     | 
| 
      
 118 
     | 
    
         
            +
                        if container_status.empty?
         
     | 
| 
      
 119 
     | 
    
         
            +
                          say "▶️ No running container. Running docker compose up...", :cyan
         
     | 
| 
      
 120 
     | 
    
         
            +
                          ssh.exec!("cd #{docker_dir_remote} && docker compose up -d")
         
     | 
| 
      
 121 
     | 
    
         
            +
                        else
         
     | 
| 
      
 122 
     | 
    
         
            +
                          say "⚠️ PostgreSQL container is already running.", :yellow
         
     | 
| 
      
 123 
     | 
    
         
            +
                          if yes?("🔁 Recreate the container with updated configuration? [y/N]", :yellow)
         
     | 
| 
      
 124 
     | 
    
         
            +
                            say "🔄 Recreating container...", :cyan
         
     | 
| 
      
 125 
     | 
    
         
            +
                            ssh.exec!("cd #{docker_dir_remote} && docker compose down -v && docker compose up -d")
         
     | 
| 
      
 126 
     | 
    
         
            +
                          else
         
     | 
| 
      
 127 
     | 
    
         
            +
                            say "⏩ Keeping existing container.", :cyan
         
     | 
| 
      
 128 
     | 
    
         
            +
                          end
         
     | 
| 
      
 129 
     | 
    
         
            +
                        end
         
     | 
| 
      
 130 
     | 
    
         
            +
             
     | 
| 
      
 131 
     | 
    
         
            +
                        say "📋 Final container status (docker compose ps):", :cyan
         
     | 
| 
      
 132 
     | 
    
         
            +
                        docker_ps_output = ssh.exec!("cd #{docker_dir_remote} && docker compose ps --format json")
         
     | 
| 
      
 133 
     | 
    
         
            +
             
     | 
| 
      
 134 
     | 
    
         
            +
                        if docker_ps_output.nil? || docker_ps_output.strip.empty? || docker_ps_output.include?("no configuration file")
         
     | 
| 
      
 135 
     | 
    
         
            +
                          say "⚠️ docker compose ps returned no valid output.", :yellow
         
     | 
| 
      
 136 
     | 
    
         
            +
                        else
         
     | 
| 
      
 137 
     | 
    
         
            +
                          begin
         
     | 
| 
      
 138 
     | 
    
         
            +
                            services = JSON.parse(docker_ps_output)
         
     | 
| 
      
 139 
     | 
    
         
            +
                            services = [services] if services.is_a?(Hash)
         
     | 
| 
      
 140 
     | 
    
         
            +
             
     | 
| 
      
 141 
     | 
    
         
            +
                            postgres = services.find { |svc| svc["Service"] == "postgres" }
         
     | 
| 
      
 142 
     | 
    
         
            +
                            status = postgres && postgres["State"]
         
     | 
| 
      
 143 
     | 
    
         
            +
                            health = postgres && postgres["Health"]
         
     | 
| 
      
 144 
     | 
    
         
            +
             
     | 
| 
      
 145 
     | 
    
         
            +
                            if (status == "running" && health == "healthy") || (health == "healthy")
         
     | 
| 
      
 146 
     | 
    
         
            +
                              say "✅ PostgreSQL container is running and healthy.", :green
         
     | 
| 
      
 147 
     | 
    
         
            +
                            else
         
     | 
| 
      
 148 
     | 
    
         
            +
                              say "⚠️ PostgreSQL container is not healthy yet.", :yellow
         
     | 
| 
      
 149 
     | 
    
         
            +
                            end
         
     | 
| 
      
 150 
     | 
    
         
            +
                          rescue JSON::ParserError => e
         
     | 
| 
      
 151 
     | 
    
         
            +
                            say "🚨 Failed to parse docker compose ps output as JSON: #{e.message}", :red
         
     | 
| 
      
 152 
     | 
    
         
            +
                          end
         
     | 
| 
      
 153 
     | 
    
         
            +
                        end
         
     | 
| 
      
 154 
     | 
    
         
            +
             
     | 
| 
      
 155 
     | 
    
         
            +
                        # 9. Check PostgreSQL readiness with retries
         
     | 
| 
      
 156 
     | 
    
         
            +
                        say "🔍 Checking PostgreSQL health with retries...", :cyan
         
     | 
| 
      
 157 
     | 
    
         
            +
             
     | 
| 
      
 158 
     | 
    
         
            +
                        max_attempts = 10
         
     | 
| 
      
 159 
     | 
    
         
            +
                        attempt = 0
         
     | 
| 
      
 160 
     | 
    
         
            +
                        success = false
         
     | 
| 
      
 161 
     | 
    
         
            +
             
     | 
| 
      
 162 
     | 
    
         
            +
                        while attempt < max_attempts
         
     | 
| 
      
 163 
     | 
    
         
            +
                          attempt += 1
         
     | 
| 
      
 164 
     | 
    
         
            +
                          healthcheck = ssh.exec!("docker exec $(docker ps -qf name=postgres) pg_isready -U #{postgres_defaults[:postgres_user]} -d #{postgres_defaults[:postgres_db]} -h localhost")
         
     | 
| 
      
 165 
     | 
    
         
            +
             
     | 
| 
      
 166 
     | 
    
         
            +
                          if healthcheck.include?("accepting connections")
         
     | 
| 
      
 167 
     | 
    
         
            +
                            say "✅ PostgreSQL is up and accepting connections! (attempt #{attempt})", :green
         
     | 
| 
      
 168 
     | 
    
         
            +
                            success = true
         
     | 
| 
      
 169 
     | 
    
         
            +
                            break
         
     | 
| 
      
 170 
     | 
    
         
            +
                          else
         
     | 
| 
      
 171 
     | 
    
         
            +
                            say "⏳ Attempt #{attempt}/#{max_attempts}: PostgreSQL not ready yet, retrying in 5 seconds...", :yellow
         
     | 
| 
      
 172 
     | 
    
         
            +
                            sleep 5
         
     | 
| 
      
 173 
     | 
    
         
            +
                          end
         
     | 
| 
      
 174 
     | 
    
         
            +
                        end
         
     | 
| 
      
 175 
     | 
    
         
            +
             
     | 
| 
      
 176 
     | 
    
         
            +
                        unless success
         
     | 
| 
      
 177 
     | 
    
         
            +
                          say "❌ PostgreSQL did not become ready after #{max_attempts} attempts.", :red
         
     | 
| 
      
 178 
     | 
    
         
            +
                        end
         
     | 
| 
      
 179 
     | 
    
         
            +
             
     | 
| 
      
 180 
     | 
    
         
            +
                        # 10. Allow PostgreSQL port through firewall (ufw)
         
     | 
| 
      
 181 
     | 
    
         
            +
                        say "🛡️ Configuring firewall to allow PostgreSQL (port #{postgres_defaults[:postgres_port]})...", :cyan
         
     | 
| 
      
 182 
     | 
    
         
            +
                        firewall = <<~EOH
         
     | 
| 
      
 183 
     | 
    
         
            +
                          if command -v ufw >/dev/null; then
         
     | 
| 
      
 184 
     | 
    
         
            +
                            if ! sudo ufw status | grep -q "#{postgres_defaults[:postgres_port]}"; then
         
     | 
| 
      
 185 
     | 
    
         
            +
                              sudo ufw allow #{postgres_defaults[:postgres_port]}
         
     | 
| 
      
 186 
     | 
    
         
            +
                            else
         
     | 
| 
      
 187 
     | 
    
         
            +
                              echo "🔸 Port #{postgres_defaults[:postgres_port]} is already allowed in ufw."
         
     | 
| 
      
 188 
     | 
    
         
            +
                            fi
         
     | 
| 
      
 189 
     | 
    
         
            +
                          else
         
     | 
| 
      
 190 
     | 
    
         
            +
                            echo "⚠️ ufw not found. Skipping firewall configuration."
         
     | 
| 
      
 191 
     | 
    
         
            +
                          fi
         
     | 
| 
      
 192 
     | 
    
         
            +
                        EOH
         
     | 
| 
      
 193 
     | 
    
         
            +
                        ssh.exec!(firewall)
         
     | 
| 
      
 194 
     | 
    
         
            +
                      end
         
     | 
| 
      
 195 
     | 
    
         
            +
             
     | 
| 
      
 196 
     | 
    
         
            +
                      def perform_rollback(ssh)
         
     | 
| 
      
 197 
     | 
    
         
            +
                        output = ssh.exec! <<~EOH
         
     | 
| 
      
 198 
     | 
    
         
            +
                          set -e
         
     | 
| 
      
 199 
     | 
    
         
            +
             
     | 
| 
      
 200 
     | 
    
         
            +
                          BASE_DIR="$HOME/docker/postgres"
         
     | 
| 
      
 201 
     | 
    
         
            +
                          BACKUP_DIR="/usr/local/backups"
         
     | 
| 
      
 202 
     | 
    
         
            +
                          SCRIPT_ID="setup_postgres_docker"
         
     | 
| 
      
 203 
     | 
    
         
            +
                          AFTER_FILE="${BACKUP_DIR}/${SCRIPT_ID}.after"
         
     | 
| 
      
 204 
     | 
    
         
            +
             
     | 
| 
      
 205 
     | 
    
         
            +
                          if [ -f "$AFTER_FILE" ]; then
         
     | 
| 
      
 206 
     | 
    
         
            +
                            echo "🔁 Stopping and removing docker containers..."
         
     | 
| 
      
 207 
     | 
    
         
            +
                            cd "$BASE_DIR"
         
     | 
| 
      
 208 
     | 
    
         
            +
                            docker compose down -v || true
         
     | 
| 
      
 209 
     | 
    
         
            +
             
     | 
| 
      
 210 
     | 
    
         
            +
                            echo "🧹 Cleaning up files..."
         
     | 
| 
      
 211 
     | 
    
         
            +
                            rm -rf "$BASE_DIR"
         
     | 
| 
      
 212 
     | 
    
         
            +
                            sudo rm -f "$AFTER_FILE"
         
     | 
| 
      
 213 
     | 
    
         
            +
             
     | 
| 
      
 214 
     | 
    
         
            +
                            if command -v ufw >/dev/null; then
         
     | 
| 
      
 215 
     | 
    
         
            +
                              echo "🛡️ Removing PostgreSQL port from firewall..."
         
     | 
| 
      
 216 
     | 
    
         
            +
                              sudo ufw delete allow 5432 || true
         
     | 
| 
      
 217 
     | 
    
         
            +
                            fi
         
     | 
| 
      
 218 
     | 
    
         
            +
                          else
         
     | 
| 
      
 219 
     | 
    
         
            +
                            echo "🔸 Nothing to rollback"
         
     | 
| 
      
 220 
     | 
    
         
            +
                          fi
         
     | 
| 
      
 221 
     | 
    
         
            +
             
     | 
| 
      
 222 
     | 
    
         
            +
                          echo "✅ Rollback completed"
         
     | 
| 
      
 223 
     | 
    
         
            +
                        EOH
         
     | 
| 
      
 224 
     | 
    
         
            +
                        say output
         
     | 
| 
      
 225 
     | 
    
         
            +
                      end
         
     | 
| 
      
 226 
     | 
    
         
            +
             
     | 
| 
      
 227 
     | 
    
         
            +
                      def upload_file(ssh, content, remote_path)
         
     | 
| 
      
 228 
     | 
    
         
            +
                        escaped_content = Shellwords.escape(content)
         
     | 
| 
      
 229 
     | 
    
         
            +
                        ssh.exec!("mkdir -p #{File.dirname(remote_path)}")
         
     | 
| 
      
 230 
     | 
    
         
            +
                        ssh.exec!("echo #{escaped_content} > #{remote_path}")
         
     | 
| 
      
 231 
     | 
    
         
            +
                      end
         
     | 
| 
      
 232 
     | 
    
         
            +
             
     | 
| 
      
 233 
     | 
    
         
            +
                      def build_database_url(filled_options, postgres_defaults)
         
     | 
| 
      
 234 
     | 
    
         
            +
                        user = postgres_defaults[:postgres_user]
         
     | 
| 
      
 235 
     | 
    
         
            +
                        password = postgres_defaults[:postgres_password]
         
     | 
| 
      
 236 
     | 
    
         
            +
                        host = filled_options[:server_ip]
         
     | 
| 
      
 237 
     | 
    
         
            +
                        port = postgres_defaults[:postgres_port]
         
     | 
| 
      
 238 
     | 
    
         
            +
                        db = postgres_defaults[:postgres_db]
         
     | 
| 
      
 239 
     | 
    
         
            +
             
     | 
| 
      
 240 
     | 
    
         
            +
                        "postgres://#{user}:#{password}@#{host}:#{port}/#{db}"
         
     | 
| 
      
 241 
     | 
    
         
            +
                      end
         
     | 
| 
      
 242 
     | 
    
         
            +
                    end
         
     | 
| 
      
 243 
     | 
    
         
            +
                  end
         
     | 
| 
      
 244 
     | 
    
         
            +
                end
         
     | 
| 
      
 245 
     | 
    
         
            +
              end
         
     | 
| 
      
 246 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,132 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require "thor"
         
     | 
| 
      
 2 
     | 
    
         
            +
            require "net/ssh"
         
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative "../defaults"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require_relative "../options_builder"
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            module Kitsune
         
     | 
| 
      
 7 
     | 
    
         
            +
              module Kit
         
     | 
| 
      
 8 
     | 
    
         
            +
                module Commands
         
     | 
| 
      
 9 
     | 
    
         
            +
                  class SetupUnattended < Thor
         
     | 
| 
      
 10 
     | 
    
         
            +
                    namespace "setup_unattended"
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                    class_option :server_ip,    aliases: "-s", required: true, desc: "Server IP address or hostname"
         
     | 
| 
      
 13 
     | 
    
         
            +
                    class_option :ssh_port,     aliases: "-p", desc: "SSH port"
         
     | 
| 
      
 14 
     | 
    
         
            +
                    class_option :ssh_key_path, aliases: "-k", desc: "Path to your private SSH key"
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                    desc "create", "Configure unattended-upgrades on the remote server"
         
     | 
| 
      
 17 
     | 
    
         
            +
                    def create
         
     | 
| 
      
 18 
     | 
    
         
            +
                      filled_options = Kitsune::Kit::OptionsBuilder.build(
         
     | 
| 
      
 19 
     | 
    
         
            +
                        options,
         
     | 
| 
      
 20 
     | 
    
         
            +
                        required: [:server_ip],
         
     | 
| 
      
 21 
     | 
    
         
            +
                        defaults: Kitsune::Kit::Defaults.ssh
         
     | 
| 
      
 22 
     | 
    
         
            +
                      )
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                      with_ssh_connection(filled_options) do |ssh|
         
     | 
| 
      
 25 
     | 
    
         
            +
                        perform_setup(ssh)
         
     | 
| 
      
 26 
     | 
    
         
            +
                      end
         
     | 
| 
      
 27 
     | 
    
         
            +
                    end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                    desc "rollback", "Revert unattended-upgrades configuration on the remote server"
         
     | 
| 
      
 30 
     | 
    
         
            +
                    def rollback
         
     | 
| 
      
 31 
     | 
    
         
            +
                      filled_options = Kitsune::Kit::OptionsBuilder.build(
         
     | 
| 
      
 32 
     | 
    
         
            +
                        options,
         
     | 
| 
      
 33 
     | 
    
         
            +
                        required: [:server_ip],
         
     | 
| 
      
 34 
     | 
    
         
            +
                        defaults: Kitsune::Kit::Defaults.ssh
         
     | 
| 
      
 35 
     | 
    
         
            +
                      )
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                      with_ssh_connection(filled_options) do |ssh|
         
     | 
| 
      
 38 
     | 
    
         
            +
                        perform_rollback(ssh)
         
     | 
| 
      
 39 
     | 
    
         
            +
                      end
         
     | 
| 
      
 40 
     | 
    
         
            +
                    end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                    no_commands do
         
     | 
| 
      
 43 
     | 
    
         
            +
                      def with_ssh_connection(filled_options)
         
     | 
| 
      
 44 
     | 
    
         
            +
                        server = filled_options[:server_ip]
         
     | 
| 
      
 45 
     | 
    
         
            +
                        port   = filled_options[:ssh_port]
         
     | 
| 
      
 46 
     | 
    
         
            +
                        key    = File.expand_path(filled_options[:ssh_key_path])
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                        say "🔑 Connecting as deploy@#{server}:#{port}", :green
         
     | 
| 
      
 49 
     | 
    
         
            +
                        Net::SSH.start(server, "deploy", port: port, keys: [key], non_interactive: true, timeout: 5) do |ssh|
         
     | 
| 
      
 50 
     | 
    
         
            +
                          yield ssh
         
     | 
| 
      
 51 
     | 
    
         
            +
                        end
         
     | 
| 
      
 52 
     | 
    
         
            +
                      end
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                      def perform_setup(ssh)
         
     | 
| 
      
 55 
     | 
    
         
            +
                        output = ssh.exec! <<~EOH
         
     | 
| 
      
 56 
     | 
    
         
            +
                          set -e
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                          sudo mkdir -p /usr/local/backups
         
     | 
| 
      
 59 
     | 
    
         
            +
                          sudo chown deploy:deploy /usr/local/backups
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                          RESOURCE="/etc/apt/apt.conf.d/20auto-upgrades"
         
     | 
| 
      
 62 
     | 
    
         
            +
                          BACKUP_DIR="/usr/local/backups"
         
     | 
| 
      
 63 
     | 
    
         
            +
                          SCRIPT_ID="setup_unattended"
         
     | 
| 
      
 64 
     | 
    
         
            +
                          BACKUP_FILE="${BACKUP_DIR}/${SCRIPT_ID}.before"
         
     | 
| 
      
 65 
     | 
    
         
            +
                          MARKER_FILE="${BACKUP_DIR}/${SCRIPT_ID}.after"
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                          echo "✍🏻 Installing required packages…"
         
     | 
| 
      
 68 
     | 
    
         
            +
                          if ! dpkg -l | grep -q "^ii\\s*unattended-upgrades"; then
         
     | 
| 
      
 69 
     | 
    
         
            +
                            sudo apt-get update -y
         
     | 
| 
      
 70 
     | 
    
         
            +
                            sudo apt-get install -y unattended-upgrades apt-listchanges && echo "   - packages installed"
         
     | 
| 
      
 71 
     | 
    
         
            +
                          else
         
     | 
| 
      
 72 
     | 
    
         
            +
                            echo "   - unattended-upgrades already installed"
         
     | 
| 
      
 73 
     | 
    
         
            +
                          fi
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                          if [ ! -f "$MARKER_FILE" ]; then
         
     | 
| 
      
 76 
     | 
    
         
            +
                            echo "✍🏻 Backing up existing config…"
         
     | 
| 
      
 77 
     | 
    
         
            +
                            sudo cp "$RESOURCE" "$BACKUP_FILE" && echo "   - backup saved to $BACKUP_FILE"
         
     | 
| 
      
 78 
     | 
    
         
            +
                            sudo touch "$MARKER_FILE" && echo "   - marker created at $MARKER_FILE"
         
     | 
| 
      
 79 
     | 
    
         
            +
                          else
         
     | 
| 
      
 80 
     | 
    
         
            +
                            echo "   - backup & marker already exist"
         
     | 
| 
      
 81 
     | 
    
         
            +
                          fi
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
                          echo "✍🏻 Applying new auto-upgrades config…"
         
     | 
| 
      
 84 
     | 
    
         
            +
                          sudo tee "$RESOURCE" > /dev/null <<UPGR
         
     | 
| 
      
 85 
     | 
    
         
            +
            APT::Periodic::Update-Package-Lists "1";
         
     | 
| 
      
 86 
     | 
    
         
            +
            APT::Periodic::Download-Upgradeable-Packages "1";
         
     | 
| 
      
 87 
     | 
    
         
            +
            APT::Periodic::AutocleanInterval "7";
         
     | 
| 
      
 88 
     | 
    
         
            +
            APT::Periodic::Unattended-Upgrade "1";
         
     | 
| 
      
 89 
     | 
    
         
            +
            UPGR
         
     | 
| 
      
 90 
     | 
    
         
            +
                          echo "   - config applied"
         
     | 
| 
      
 91 
     | 
    
         
            +
             
     | 
| 
      
 92 
     | 
    
         
            +
                          echo "✍🏻 Enabling & restarting unattended-upgrades…"
         
     | 
| 
      
 93 
     | 
    
         
            +
                          sudo systemctl --quiet enable unattended-upgrades.service >/dev/null 2>&1 && echo "   - service enabled"
         
     | 
| 
      
 94 
     | 
    
         
            +
                          sudo systemctl --quiet restart unattended-upgrades.service && echo "   - service restarted"
         
     | 
| 
      
 95 
     | 
    
         
            +
                        EOH
         
     | 
| 
      
 96 
     | 
    
         
            +
                        say output
         
     | 
| 
      
 97 
     | 
    
         
            +
                        say "✅ Unattended-upgrades setup completed", :green
         
     | 
| 
      
 98 
     | 
    
         
            +
                      end
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
                      def perform_rollback(ssh)
         
     | 
| 
      
 101 
     | 
    
         
            +
                        output = ssh.exec! <<~EOH
         
     | 
| 
      
 102 
     | 
    
         
            +
                          set -e
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
                          sudo mkdir -p /usr/local/backups
         
     | 
| 
      
 105 
     | 
    
         
            +
                          sudo chown deploy:deploy /usr/local/backups
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
                          RESOURCE="/etc/apt/apt.conf.d/20auto-upgrades"
         
     | 
| 
      
 108 
     | 
    
         
            +
                          BACKUP_DIR="/usr/local/backups"
         
     | 
| 
      
 109 
     | 
    
         
            +
                          SCRIPT_ID="setup_unattended"
         
     | 
| 
      
 110 
     | 
    
         
            +
                          BACKUP_FILE="${BACKUP_DIR}/${SCRIPT_ID}.before"
         
     | 
| 
      
 111 
     | 
    
         
            +
                          MARKER_FILE="${BACKUP_DIR}/${SCRIPT_ID}.after"
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
                          if [ -f "$MARKER_FILE" ]; then
         
     | 
| 
      
 114 
     | 
    
         
            +
                            echo "🔁 Restoring original auto-upgrades config…"
         
     | 
| 
      
 115 
     | 
    
         
            +
                            sudo mv "$BACKUP_FILE" "$RESOURCE" && echo "   - config restored from $BACKUP_FILE"
         
     | 
| 
      
 116 
     | 
    
         
            +
                            sudo rm -f "$MARKER_FILE" && echo "   - marker $MARKER_FILE removed"
         
     | 
| 
      
 117 
     | 
    
         
            +
                          else
         
     | 
| 
      
 118 
     | 
    
         
            +
                            echo "   - no marker for $SCRIPT_ID, skipping restore"
         
     | 
| 
      
 119 
     | 
    
         
            +
                          fi
         
     | 
| 
      
 120 
     | 
    
         
            +
             
     | 
| 
      
 121 
     | 
    
         
            +
                          echo "✍🏻 Stopping & disabling unattended-upgrades…"
         
     | 
| 
      
 122 
     | 
    
         
            +
                          sudo systemctl --quiet stop unattended-upgrades.service apt-daily.timer apt-daily-upgrade.timer && echo "   - services stopped"
         
     | 
| 
      
 123 
     | 
    
         
            +
                          sudo systemctl --quiet disable unattended-upgrades.service && echo "   - service disabled"
         
     | 
| 
      
 124 
     | 
    
         
            +
                        EOH
         
     | 
| 
      
 125 
     | 
    
         
            +
                        say output
         
     | 
| 
      
 126 
     | 
    
         
            +
                        say "✅ Unattended-upgrades rollback completed", :green
         
     | 
| 
      
 127 
     | 
    
         
            +
                      end
         
     | 
| 
      
 128 
     | 
    
         
            +
                    end
         
     | 
| 
      
 129 
     | 
    
         
            +
                  end
         
     | 
| 
      
 130 
     | 
    
         
            +
                end
         
     | 
| 
      
 131 
     | 
    
         
            +
              end
         
     | 
| 
      
 132 
     | 
    
         
            +
            end
         
     |