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
|