kitsune-kit 0.4.1 → 0.6.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.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +143 -2
  3. data/CONTRIBUTING.md +83 -0
  4. data/README.md +112 -135
  5. data/SECURITY.md +25 -0
  6. data/bin/kit +4 -1
  7. data/docs/architecture/decisions/0001-product-boundary.md +23 -0
  8. data/docs/architecture/decisions/0002-core-and-interfaces.md +23 -0
  9. data/docs/architecture/decisions/0003-configuration-state-and-secrets.md +23 -0
  10. data/docs/architecture/decisions/0004-supported-platforms.md +19 -0
  11. data/docs/architecture/decisions/0005-operation-semantics.md +19 -0
  12. data/docs/architecture/decisions/0006-tui-backend.md +21 -0
  13. data/docs/architecture/decisions/0007-compose-customization.md +30 -0
  14. data/docs/architecture/provider-adapters.md +49 -0
  15. data/docs/architecture.md +93 -0
  16. data/docs/commands.md +224 -0
  17. data/docs/configuration.md +226 -0
  18. data/docs/getting-started.md +133 -0
  19. data/docs/providers/digitalocean.md +52 -0
  20. data/docs/releasing.md +43 -0
  21. data/docs/roadmap.md +46 -0
  22. data/docs/security-audit.md +130 -0
  23. data/docs/security.md +116 -0
  24. data/docs/services/compose.md +164 -0
  25. data/docs/services/postgres.md +78 -0
  26. data/docs/services/redis.md +53 -0
  27. data/docs/testing.md +200 -0
  28. data/docs/troubleshooting.md +123 -0
  29. data/docs/tui.md +57 -0
  30. data/lib/kitsune/kit/adapters/confirming_host_key_verifier.rb +42 -0
  31. data/lib/kitsune/kit/adapters/digitalocean_provider.rb +217 -0
  32. data/lib/kitsune/kit/adapters/fake_clock.rb +34 -0
  33. data/lib/kitsune/kit/adapters/fake_provider.rb +107 -0
  34. data/lib/kitsune/kit/adapters/fake_reporter.rb +34 -0
  35. data/lib/kitsune/kit/adapters/fake_secret_store.rb +35 -0
  36. data/lib/kitsune/kit/adapters/fake_state_store.rb +91 -0
  37. data/lib/kitsune/kit/adapters/fake_transport.rb +56 -0
  38. data/lib/kitsune/kit/adapters/net_ssh_transport.rb +155 -0
  39. data/lib/kitsune/kit/adapters/provider.rb +34 -0
  40. data/lib/kitsune/kit/adapters/transport.rb +22 -0
  41. data/lib/kitsune/kit/adapters/transport_factory.rb +116 -0
  42. data/lib/kitsune/kit/application.rb +170 -0
  43. data/lib/kitsune/kit/cancellation.rb +21 -0
  44. data/lib/kitsune/kit/cli.rb +879 -64
  45. data/lib/kitsune/kit/clock.rb +11 -0
  46. data/lib/kitsune/kit/configuration.rb +577 -0
  47. data/lib/kitsune/kit/errors.rb +98 -0
  48. data/lib/kitsune/kit/events.rb +55 -0
  49. data/lib/kitsune/kit/operations/ensure_dns_records.rb +165 -0
  50. data/lib/kitsune/kit/operations/ensure_server.rb +135 -0
  51. data/lib/kitsune/kit/operations/ensure_service.rb +302 -0
  52. data/lib/kitsune/kit/operations/remote_script.rb +214 -0
  53. data/lib/kitsune/kit/operations/service_backup.rb +71 -0
  54. data/lib/kitsune/kit/operations/service_files.rb +131 -0
  55. data/lib/kitsune/kit/operations/service_firewall.rb +190 -0
  56. data/lib/kitsune/kit/operations/service_state.rb +59 -0
  57. data/lib/kitsune/kit/plan.rb +72 -0
  58. data/lib/kitsune/kit/reporters/human.rb +89 -0
  59. data/lib/kitsune/kit/reporters/json.rb +65 -0
  60. data/lib/kitsune/kit/reporters/reporter.rb +11 -0
  61. data/lib/kitsune/kit/result.rb +28 -0
  62. data/lib/kitsune/kit/run_journal.rb +102 -0
  63. data/lib/kitsune/kit/run_logger.rb +37 -0
  64. data/lib/kitsune/kit/scripts/docker.sh +142 -0
  65. data/lib/kitsune/kit/scripts/firewall.sh +210 -0
  66. data/lib/kitsune/kit/scripts/metrics.sh +59 -0
  67. data/lib/kitsune/kit/scripts/ssh.sh +95 -0
  68. data/lib/kitsune/kit/scripts/swap.sh +86 -0
  69. data/lib/kitsune/kit/scripts/unattended.sh +86 -0
  70. data/lib/kitsune/kit/scripts/user.sh +114 -0
  71. data/lib/kitsune/kit/secret_filter.rb +54 -0
  72. data/lib/kitsune/kit/secret_store.rb +32 -0
  73. data/lib/kitsune/kit/secret_stores/store.rb +12 -0
  74. data/lib/kitsune/kit/service_compose.rb +272 -0
  75. data/lib/kitsune/kit/state_store.rb +158 -0
  76. data/lib/kitsune/kit/state_stores/store.rb +15 -0
  77. data/lib/kitsune/kit/tui/actions.rb +72 -0
  78. data/lib/kitsune/kit/tui/application.rb +35 -0
  79. data/lib/kitsune/kit/tui/controller.rb +162 -0
  80. data/lib/kitsune/kit/tui/renderer.rb +134 -0
  81. data/lib/kitsune/kit/tui/state.rb +18 -0
  82. data/lib/kitsune/kit/tui/store.rb +88 -0
  83. data/lib/kitsune/kit/tui/terminal.rb +95 -0
  84. data/lib/kitsune/kit/version.rb +1 -1
  85. data/lib/kitsune/kit/workflows/apply_plan.rb +145 -0
  86. data/lib/kitsune/kit/workflows/base.rb +31 -0
  87. data/lib/kitsune/kit/workflows/build_plan.rb +33 -0
  88. data/lib/kitsune/kit/workflows/destroy_server.rb +84 -0
  89. data/lib/kitsune/kit/workflows/doctor.rb +225 -0
  90. data/lib/kitsune/kit/workflows/eject_compose.rb +82 -0
  91. data/lib/kitsune/kit/workflows/environment_selection.rb +70 -0
  92. data/lib/kitsune/kit/workflows/import_server.rb +100 -0
  93. data/lib/kitsune/kit/workflows/initialize_project.rb +129 -0
  94. data/lib/kitsune/kit/workflows/inspect_environment.rb +44 -0
  95. data/lib/kitsune/kit/workflows/rollback.rb +54 -0
  96. data/lib/kitsune/kit/workflows/support_bundle.rb +82 -0
  97. data/lib/kitsune/kit.rb +42 -2
  98. metadata +125 -79
  99. data/.rspec +0 -3
  100. data/Rakefile +0 -8
  101. data/kitsune-kit-logo.jpg +0 -0
  102. data/lib/kitsune/blueprints/.env.template +0 -31
  103. data/lib/kitsune/blueprints/docker/postgres.yml +0 -27
  104. data/lib/kitsune/blueprints/docker/redis.yml +0 -23
  105. data/lib/kitsune/blueprints/kit.env.template +0 -1
  106. data/lib/kitsune/kit/ansi_color.rb +0 -78
  107. data/lib/kitsune/kit/commands/bootstrap.rb +0 -134
  108. data/lib/kitsune/kit/commands/bootstrap_docker.rb +0 -66
  109. data/lib/kitsune/kit/commands/dns.rb +0 -112
  110. data/lib/kitsune/kit/commands/init.rb +0 -148
  111. data/lib/kitsune/kit/commands/install_docker_engine.rb +0 -146
  112. data/lib/kitsune/kit/commands/postinstall_docker.rb +0 -142
  113. data/lib/kitsune/kit/commands/provision.rb +0 -43
  114. data/lib/kitsune/kit/commands/setup_do_metrics.rb +0 -123
  115. data/lib/kitsune/kit/commands/setup_docker_prereqs.rb +0 -151
  116. data/lib/kitsune/kit/commands/setup_firewall.rb +0 -132
  117. data/lib/kitsune/kit/commands/setup_postgres_docker.rb +0 -246
  118. data/lib/kitsune/kit/commands/setup_redis_docker.rb +0 -241
  119. data/lib/kitsune/kit/commands/setup_swap.rb +0 -151
  120. data/lib/kitsune/kit/commands/setup_unattended.rb +0 -132
  121. data/lib/kitsune/kit/commands/setup_user.rb +0 -189
  122. data/lib/kitsune/kit/commands/ssh.rb +0 -46
  123. data/lib/kitsune/kit/commands/switch_env.rb +0 -42
  124. data/lib/kitsune/kit/defaults.rb +0 -91
  125. data/lib/kitsune/kit/env_loader.rb +0 -41
  126. data/lib/kitsune/kit/options_builder.rb +0 -26
  127. data/lib/kitsune/kit/provisioner.rb +0 -107
  128. data/sig/kitsune/kit.rbs +0 -6
@@ -1,241 +0,0 @@
1
- require "thor"
2
- require "net/ssh"
3
- require "fileutils"
4
- require "shellwords"
5
- require_relative "../defaults"
6
- require_relative "../options_builder"
7
- require "pry"
8
- module Kitsune
9
- module Kit
10
- module Commands
11
- class SetupRedisDocker < Thor
12
- namespace "setup_redis_docker"
13
-
14
- class_option :server_ip, aliases: "-s", required: true, desc: "Server IP address or hostname"
15
- class_option :ssh_port, aliases: "-p", desc: "SSH port"
16
- class_option :ssh_key_path, aliases: "-k", desc: "Path to SSH private key"
17
-
18
- desc "create", "Setup Redis using Docker Compose on remote server"
19
- def create
20
- redis_defaults = Kitsune::Kit::Defaults.redis
21
-
22
- if redis_defaults[:redis_password] == "secret"
23
- say "⚠️ Warning: You are using the default Redis password ('secret').", :yellow
24
- if ENV.fetch("KIT_ENV", "development") == "production"
25
- abort "❌ Production environment requires a secure Redis password!"
26
- else
27
- say "🔒 Please change REDIS_PASSWORD in your .env if needed.", :yellow
28
- end
29
- end
30
-
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_setup(ssh, redis_defaults, filled_options)
39
- end
40
- end
41
-
42
- desc "rollback", "Remove Redis Docker setup from remote server"
43
- def rollback
44
- redis_defaults = Kitsune::Kit::Defaults.redis
45
-
46
- filled_options = Kitsune::Kit::OptionsBuilder.build(
47
- options,
48
- required: [:server_ip],
49
- defaults: Kitsune::Kit::Defaults.ssh
50
- )
51
-
52
- with_ssh_connection(filled_options) do |ssh|
53
- perform_rollback(ssh, redis_defaults)
54
- end
55
- end
56
-
57
- no_commands do
58
- def with_ssh_connection(filled_options)
59
- server = filled_options[:server_ip]
60
- port = filled_options[:ssh_port]
61
- key = File.expand_path(filled_options[:ssh_key_path])
62
-
63
- say "🔑 Connecting as deploy@#{server}:#{port}", :green
64
- Net::SSH.start(server, "deploy", port: port, keys: [key], non_interactive: true, timeout: 5) do |ssh|
65
- yield ssh
66
- end
67
- end
68
-
69
- def perform_setup(ssh, redis_defaults, filled_options)
70
- local_compose = ".kitsune/docker/redis.yml"
71
- remote_dir = "$HOME/docker/redis"
72
- compose_remote = "#{remote_dir}/docker-compose.yml"
73
- env_remote = "#{remote_dir}/.env"
74
- marker = "/usr/local/backups/setup_redis_docker.after"
75
-
76
- abort "❌ Missing #{local_compose}" unless File.exist?(local_compose)
77
-
78
- # 1. Create base directory securely
79
- ssh.exec!("mkdir -p #{remote_dir} && chmod 700 #{remote_dir}")
80
-
81
- # 2. Upload docker-compose.yml
82
- say "📦 Uploading docker-compose.yml to #{remote_dir}", :cyan
83
- upload_file(ssh, File.read(local_compose), compose_remote)
84
-
85
- # 3. Create .env file for docker-compose based on redis_defaults
86
- env_content = <<~ENVFILE
87
- REDIS_PORT=#{redis_defaults[:redis_port]}
88
- REDIS_PASSWORD=#{redis_defaults[:redis_password]}
89
- ENVFILE
90
- upload_file(ssh, env_content, env_remote)
91
-
92
- # 4. Secure file permissions
93
- ssh.exec!("chmod 600 #{compose_remote} #{env_remote}")
94
-
95
- # 5. Create a backup marker
96
- ssh.exec!("sudo mkdir -p /usr/local/backups && sudo touch #{marker}")
97
-
98
- # 6. Validate docker-compose.yml
99
- say "🔍 Validating docker-compose.yml...", :cyan
100
- validation_output = ssh.exec!("cd #{remote_dir} && docker compose config")
101
- say validation_output, :cyan
102
-
103
- # 7. Check if container is running
104
- container_status = ssh.exec!("docker ps --filter 'name=redis' --format '{{.Status}}'").strip
105
-
106
- if container_status.empty?
107
- say "▶️ No running container. Running docker compose up...", :cyan
108
- ssh.exec!("cd #{remote_dir} && docker compose up -d")
109
- else
110
- say "⚠️ Redis container is already running.", :yellow
111
- if yes?("🔁 Recreate the container with updated configuration? [y/N]", :yellow)
112
- say "🔄 Recreating container...", :cyan
113
- ssh.exec!("cd #{remote_dir} && docker compose down -v && docker compose up -d")
114
- else
115
- say "⏩ Keeping existing container.", :cyan
116
- end
117
- end
118
-
119
- # 8. Check container status
120
- say "📋 Final container status (docker compose ps):", :cyan
121
- docker_ps_output = ssh.exec!("cd #{remote_dir} && docker compose ps --format json")
122
-
123
- if docker_ps_output.nil? || docker_ps_output.strip.empty? || docker_ps_output.include?("no configuration file")
124
- say "⚠️ docker compose ps returned no valid output.", :yellow
125
- else
126
- begin
127
- services = JSON.parse(docker_ps_output)
128
- services = [services] if services.is_a?(Hash)
129
-
130
- redis = services.find { |svc| svc["Service"] == "redis" }
131
- status = redis && redis["State"]
132
- health = redis && redis["Health"]
133
-
134
- if (status == "running" && health == "healthy") || (health == "healthy")
135
- say "✅ Redis container is running and healthy.", :green
136
- else
137
- say "⚠️ Redis container is not healthy yet.", :yellow
138
- end
139
- rescue JSON::ParserError => e
140
- say "🚨 Failed to parse docker compose ps output as JSON: #{e.message}", :red
141
- end
142
- end
143
-
144
- # 9. Check Redis readiness with retries
145
- say "🔍 Checking Redis health with retries...", :cyan
146
-
147
- max_attempts = 10
148
- attempt = 0
149
- success = false
150
-
151
- while attempt < max_attempts
152
- attempt += 1
153
- healthcheck = ssh.exec!("docker exec $(docker ps -qf name=redis) redis-cli --no-auth-warning -a \"#{redis_defaults[:redis_password]}\" PING")
154
-
155
- if healthcheck.strip == "PONG"
156
- say "✅ Redis is up and responding to PING! (attempt #{attempt})", :green
157
- success = true
158
-
159
- redis_url = build_redis_url(filled_options, redis_defaults)
160
- say "🔗 Your REDIS_URL is:\t", :cyan
161
- say redis_url, :green
162
- break
163
- else
164
- say "⏳ Redis not ready yet, retrying in 5 seconds... (#{attempt}/#{max_attempts})", :yellow
165
- sleep 5
166
- end
167
- end
168
-
169
- unless success
170
- say "❌ Redis did not become ready after #{max_attempts} attempts.", :red
171
- end
172
-
173
- # 10. Allow Redis port through firewall (ufw)
174
- say "🛡️ Configuring firewall to allow Redis (port #{redis_defaults[:redis_port]})...", :cyan
175
- output = ssh.exec! <<~EOH
176
- if command -v ufw >/dev/null; then
177
- if ! sudo ufw status | grep -q "#{redis_defaults[:redis_port]}"; then
178
- sudo ufw allow #{redis_defaults[:redis_port]}
179
- else
180
- echo "💡 Port #{redis_defaults[:redis_port]} is already allowed in ufw."
181
- fi
182
- else
183
- echo "⚠️ ufw not found. Skipping firewall configuration."
184
- fi
185
- EOH
186
- say output
187
-
188
- say "✅ Redis setup completed successfully!", :green
189
- end
190
-
191
- def perform_rollback(ssh, defaults)
192
- output = ssh.exec! <<~EOH
193
- set -e
194
-
195
- BASE_DIR="$HOME/docker/redis"
196
- BACKUP_DIR="/usr/local/backups"
197
- SCRIPT_ID="setup_redis_docker"
198
- AFTER_FILE="${BACKUP_DIR}/${SCRIPT_ID}.after"
199
-
200
- if [ -f "$AFTER_FILE" ]; then
201
- echo "🔁 Stopping and removing docker containers..."
202
- cd "$BASE_DIR"
203
- docker compose down -v || true
204
-
205
- echo "🧹 Cleaning up files..."
206
- rm -rf "$BASE_DIR"
207
- sudo rm -f "$AFTER_FILE"
208
-
209
- if command -v ufw >/dev/null; then
210
- echo "🛡️ Removing Redis port from firewall..."
211
- sudo ufw delete allow #{defaults[:redis_port]} || true
212
- fi
213
- else
214
- echo "💡 Nothing to rollback"
215
- fi
216
-
217
- echo "✅ Rollback completed"
218
- EOH
219
-
220
- say output
221
- end
222
-
223
-
224
- def upload_file(ssh, content, remote_path)
225
- escaped = Shellwords.escape(content)
226
- ssh.exec!("mkdir -p #{File.dirname(remote_path)}")
227
- ssh.exec!("echo #{escaped} > #{remote_path}")
228
- end
229
-
230
- def build_redis_url(filled_options, redis_defaults)
231
- password = redis_defaults[:redis_password]
232
- host = filled_options[:server_ip]
233
- port = redis_defaults[:redis_port]
234
-
235
- "redis://:#{password}@#{host}:#{port}/0"
236
- end
237
- end
238
- end
239
- end
240
- end
241
- end
@@ -1,151 +0,0 @@
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 SetupSwap < Thor
10
- namespace "setup_swap"
11
-
12
- class_option :server_ip, aliases: "-s", required: true, desc: "Server IP address"
13
- class_option :ssh_port, aliases: "-p", desc: "SSH port"
14
- class_option :ssh_key_path, aliases: "-k", desc: "SSH private key path"
15
- class_option :size_gb, type: :numeric, desc: "Swap size in GB"
16
- class_option :swappiness, type: :numeric, desc: "vm.swappiness value (0-100)"
17
-
18
- desc "create", "Create and activate a swap file on the remote server"
19
- def create
20
- if Kitsune::Kit::Defaults.system[:disable_swap]
21
- say "⚠️ Swap setup is disabled via DISABLE_SWAP=true", :yellow
22
- return
23
- end
24
-
25
- filled_options = Kitsune::Kit::OptionsBuilder.build(
26
- options,
27
- required: [:server_ip],
28
- defaults: Kitsune::Kit::Defaults.system.merge(Kitsune::Kit::Defaults.ssh)
29
- )
30
-
31
- with_ssh_connection(filled_options) do |ssh|
32
- perform_setup(ssh, filled_options)
33
- end
34
- end
35
-
36
- desc "rollback", "Disable and remove swap file from remote server"
37
- def rollback
38
- filled_options = Kitsune::Kit::OptionsBuilder.build(
39
- options,
40
- required: [:server_ip],
41
- defaults: Kitsune::Kit::Defaults.system.merge(Kitsune::Kit::Defaults.ssh)
42
- )
43
-
44
- with_ssh_connection(filled_options) do |ssh|
45
- perform_rollback(ssh)
46
- end
47
- end
48
-
49
- no_commands do
50
- def with_ssh_connection(filled)
51
- server = filled[:server_ip]
52
- port = filled[:ssh_port]
53
- key = File.expand_path(filled[:ssh_key_path])
54
-
55
- say "🔑 Connecting as deploy@#{server}:#{port}", :green
56
- Net::SSH.start(server, "deploy", port: port, keys: [key], non_interactive: true, timeout: 5) do |ssh|
57
- yield ssh
58
- end
59
- end
60
-
61
- def perform_setup(ssh, filled_options)
62
- size_gb = filled_options[:swap_size_gb].to_i
63
- swappiness = filled_options[:swap_swappiness].to_i
64
-
65
- abort "❌ Invalid swap size" if size_gb <= 0
66
-
67
- script = <<~EOH
68
- set -e
69
-
70
- BACKUP_DIR="/usr/local/backups"
71
- MARKER_FILE="${BACKUP_DIR}/setup_swap.after"
72
- SWAPPINESS_BEFORE_FILE="${BACKUP_DIR}/setup_swap.swappiness.before"
73
-
74
- if [ -f "$MARKER_FILE" ]; then
75
- echo "🔁 Swap already set up, skipping."
76
- exit 0
77
- fi
78
-
79
- SWAPFILE="/swapfile"
80
- SIZE_GB=#{size_gb}
81
- SIZE_BYTES=$((SIZE_GB * 1024 * 1024 * 1024))
82
-
83
- echo "📝 Creating ${SIZE_GB}GB swap file..."
84
- sudo fallocate -l ${SIZE_BYTES} ${SWAPFILE}
85
- sudo chmod 600 ${SWAPFILE}
86
- sudo mkswap ${SWAPFILE}
87
- sudo swapon ${SWAPFILE}
88
- echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
89
-
90
- # Backup swappiness if not already backed up
91
- if [ ! -f "$SWAPPINESS_BEFORE_FILE" ]; then
92
- CURRENT=$(cat /proc/sys/vm/swappiness)
93
- echo "$CURRENT" | sudo tee "$SWAPPINESS_BEFORE_FILE"
94
- echo "✍🏻 Backed up vm.swappiness: $CURRENT"
95
- fi
96
-
97
- echo "🛠️ Setting vm.swappiness=#{swappiness}"
98
- sudo sysctl vm.swappiness=#{swappiness}
99
- echo "vm.swappiness=#{swappiness}" | sudo tee -a /etc/sysctl.conf
100
-
101
- sudo mkdir -p "$BACKUP_DIR"
102
- sudo touch "$MARKER_FILE"
103
-
104
- echo "✅ Swap file created and swappiness set"
105
- free -h
106
- EOH
107
-
108
- say ssh.exec!(script)
109
- end
110
-
111
- def perform_rollback(ssh)
112
- script = <<~EOH
113
- set -e
114
-
115
- BACKUP_DIR="/usr/local/backups"
116
- MARKER_FILE="${BACKUP_DIR}/setup_swap.after"
117
- SWAPPINESS_BEFORE_FILE="${BACKUP_DIR}/setup_swap.swappiness.before"
118
-
119
- if [ ! -f "$MARKER_FILE" ]; then
120
- echo "💡 No swap marker found, skipping rollback."
121
- exit 0
122
- fi
123
-
124
- echo "🧹 Removing swap..."
125
- sudo swapoff /swapfile || true
126
- sudo rm -f /swapfile
127
- sudo sed -i '/\\/swapfile none swap sw 0 0/d' /etc/fstab
128
- sudo rm -f "$MARKER_FILE"
129
-
130
- if [ -f "$SWAPPINESS_BEFORE_FILE" ]; then
131
- ORIGINAL=$(cat "$SWAPPINESS_BEFORE_FILE")
132
- echo "🔁 Restoring vm.swappiness: $ORIGINAL"
133
- sudo sysctl vm.swappiness=$ORIGINAL
134
- sudo sed -i '/vm.swappiness=/d' /etc/sysctl.conf
135
- echo "vm.swappiness=$ORIGINAL" | sudo tee -a /etc/sysctl.conf
136
- sudo rm -f "$SWAPPINESS_BEFORE_FILE"
137
- else
138
- echo "💡 No swappiness backup found, skipping restore."
139
- fi
140
-
141
- echo "✅ Swap rollback completed"
142
- free -h
143
- EOH
144
-
145
- say ssh.exec!(script)
146
- end
147
- end
148
- end
149
- end
150
- end
151
- end
@@ -1,132 +0,0 @@
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