You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

521 lines
17 KiB

  1. # frozen_string_literal: true
  2. require 'tty-prompt'
  3. namespace :mastodon do
  4. desc 'Configure the instance for production use'
  5. task :setup do
  6. prompt = TTY::Prompt.new
  7. env = {}
  8. # When the application code gets loaded, it runs `lib/mastodon/redis_configuration.rb`.
  9. # This happens before application environment configuration and sets REDIS_URL etc.
  10. # These variables are then used even when REDIS_HOST etc. are changed, so clear them
  11. # out so they don't interfere with our new configuration.
  12. ENV.delete('REDIS_URL')
  13. ENV.delete('CACHE_REDIS_URL')
  14. ENV.delete('SIDEKIQ_REDIS_URL')
  15. begin
  16. prompt.say('Your instance is identified by its domain name. Changing it afterward will break things.')
  17. env['LOCAL_DOMAIN'] = prompt.ask('Domain name:') do |q|
  18. q.required true
  19. q.modify :strip
  20. q.validate(/\A[a-z0-9\.\-]+\z/i)
  21. q.messages[:valid?] = 'Invalid domain. If you intend to use unicode characters, enter punycode here'
  22. end
  23. prompt.say "\n"
  24. prompt.say('Single user mode disables registrations and redirects the landing page to your public profile.')
  25. env['SINGLE_USER_MODE'] = prompt.yes?('Do you want to enable single user mode?', default: false)
  26. %w(SECRET_KEY_BASE OTP_SECRET).each do |key|
  27. env[key] = SecureRandom.hex(64)
  28. end
  29. vapid_key = Webpush.generate_key
  30. env['VAPID_PRIVATE_KEY'] = vapid_key.private_key
  31. env['VAPID_PUBLIC_KEY'] = vapid_key.public_key
  32. prompt.say "\n"
  33. using_docker = prompt.yes?('Are you using Docker to run Mastodon?')
  34. db_connection_works = false
  35. prompt.say "\n"
  36. loop do
  37. env['DB_HOST'] = prompt.ask('PostgreSQL host:') do |q|
  38. q.required true
  39. q.default using_docker ? 'db' : '/var/run/postgresql'
  40. q.modify :strip
  41. end
  42. env['DB_PORT'] = prompt.ask('PostgreSQL port:') do |q|
  43. q.required true
  44. q.default 5432
  45. q.convert :int
  46. end
  47. env['DB_NAME'] = prompt.ask('Name of PostgreSQL database:') do |q|
  48. q.required true
  49. q.default using_docker ? 'postgres' : 'mastodon_production'
  50. q.modify :strip
  51. end
  52. env['DB_USER'] = prompt.ask('Name of PostgreSQL user:') do |q|
  53. q.required true
  54. q.default using_docker ? 'postgres' : 'mastodon'
  55. q.modify :strip
  56. end
  57. env['DB_PASS'] = prompt.ask('Password of PostgreSQL user:') do |q|
  58. q.echo false
  59. end
  60. # The chosen database may not exist yet. Connect to default database
  61. # to avoid "database does not exist" error.
  62. db_options = {
  63. adapter: :postgresql,
  64. database: 'postgres',
  65. host: env['DB_HOST'],
  66. port: env['DB_PORT'],
  67. user: env['DB_USER'],
  68. password: env['DB_PASS'],
  69. }
  70. begin
  71. ActiveRecord::Base.establish_connection(db_options)
  72. ActiveRecord::Base.connection
  73. prompt.ok 'Database configuration works! 🎆'
  74. db_connection_works = true
  75. break
  76. rescue StandardError => e
  77. prompt.error 'Database connection could not be established with this configuration, try again.'
  78. prompt.error e.message
  79. break unless prompt.yes?('Try again?')
  80. end
  81. end
  82. prompt.say "\n"
  83. loop do
  84. env['REDIS_HOST'] = prompt.ask('Redis host:') do |q|
  85. q.required true
  86. q.default using_docker ? 'redis' : 'localhost'
  87. q.modify :strip
  88. end
  89. env['REDIS_PORT'] = prompt.ask('Redis port:') do |q|
  90. q.required true
  91. q.default 6379
  92. q.convert :int
  93. end
  94. env['REDIS_PASSWORD'] = prompt.ask('Redis password:') do |q|
  95. q.required false
  96. q.default nil
  97. q.modify :strip
  98. end
  99. redis_options = {
  100. host: env['REDIS_HOST'],
  101. port: env['REDIS_PORT'],
  102. password: env['REDIS_PASSWORD'],
  103. driver: :hiredis,
  104. }
  105. begin
  106. redis = Redis.new(redis_options)
  107. redis.ping
  108. prompt.ok 'Redis configuration works! 🎆'
  109. break
  110. rescue StandardError => e
  111. prompt.error 'Redis connection could not be established with this configuration, try again.'
  112. prompt.error e.message
  113. break unless prompt.yes?('Try again?')
  114. end
  115. end
  116. prompt.say "\n"
  117. if prompt.yes?('Do you want to store uploaded files on the cloud?', default: false)
  118. case prompt.select('Provider', ['DigitalOcean Spaces', 'Amazon S3', 'Wasabi', 'Minio', 'Google Cloud Storage'])
  119. when 'DigitalOcean Spaces'
  120. env['S3_ENABLED'] = 'true'
  121. env['S3_PROTOCOL'] = 'https'
  122. env['S3_BUCKET'] = prompt.ask('Space name:') do |q|
  123. q.required true
  124. q.default "files.#{env['LOCAL_DOMAIN']}"
  125. q.modify :strip
  126. end
  127. env['S3_REGION'] = prompt.ask('Space region:') do |q|
  128. q.required true
  129. q.default 'nyc3'
  130. q.modify :strip
  131. end
  132. env['S3_HOSTNAME'] = prompt.ask('Space endpoint:') do |q|
  133. q.required true
  134. q.default 'nyc3.digitaloceanspaces.com'
  135. q.modify :strip
  136. end
  137. env['S3_ENDPOINT'] = "https://#{env['S3_HOSTNAME']}"
  138. env['AWS_ACCESS_KEY_ID'] = prompt.ask('Space access key:') do |q|
  139. q.required true
  140. q.modify :strip
  141. end
  142. env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('Space secret key:') do |q|
  143. q.required true
  144. q.modify :strip
  145. end
  146. when 'Amazon S3'
  147. env['S3_ENABLED'] = 'true'
  148. env['S3_PROTOCOL'] = 'https'
  149. env['S3_BUCKET'] = prompt.ask('S3 bucket name:') do |q|
  150. q.required true
  151. q.default "files.#{env['LOCAL_DOMAIN']}"
  152. q.modify :strip
  153. end
  154. env['S3_REGION'] = prompt.ask('S3 region:') do |q|
  155. q.required true
  156. q.default 'us-east-1'
  157. q.modify :strip
  158. end
  159. env['S3_HOSTNAME'] = prompt.ask('S3 hostname:') do |q|
  160. q.required true
  161. q.default 's3.us-east-1.amazonaws.com'
  162. q.modify :strip
  163. end
  164. env['AWS_ACCESS_KEY_ID'] = prompt.ask('S3 access key:') do |q|
  165. q.required true
  166. q.modify :strip
  167. end
  168. env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('S3 secret key:') do |q|
  169. q.required true
  170. q.modify :strip
  171. end
  172. when 'Wasabi'
  173. env['S3_ENABLED'] = 'true'
  174. env['S3_PROTOCOL'] = 'https'
  175. env['S3_REGION'] = 'us-east-1'
  176. env['S3_HOSTNAME'] = 's3.wasabisys.com'
  177. env['S3_ENDPOINT'] = 'https://s3.wasabisys.com/'
  178. env['S3_BUCKET'] = prompt.ask('Wasabi bucket name:') do |q|
  179. q.required true
  180. q.default "files.#{env['LOCAL_DOMAIN']}"
  181. q.modify :strip
  182. end
  183. env['AWS_ACCESS_KEY_ID'] = prompt.ask('Wasabi access key:') do |q|
  184. q.required true
  185. q.modify :strip
  186. end
  187. env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('Wasabi secret key:') do |q|
  188. q.required true
  189. q.modify :strip
  190. end
  191. when 'Minio'
  192. env['S3_ENABLED'] = 'true'
  193. env['S3_PROTOCOL'] = 'https'
  194. env['S3_REGION'] = 'us-east-1'
  195. env['S3_ENDPOINT'] = prompt.ask('Minio endpoint URL:') do |q|
  196. q.required true
  197. q.modify :strip
  198. end
  199. env['S3_PROTOCOL'] = env['S3_ENDPOINT'].start_with?('https') ? 'https' : 'http'
  200. env['S3_HOSTNAME'] = env['S3_ENDPOINT'].gsub(/\Ahttps?:\/\//, '')
  201. env['S3_BUCKET'] = prompt.ask('Minio bucket name:') do |q|
  202. q.required true
  203. q.default "files.#{env['LOCAL_DOMAIN']}"
  204. q.modify :strip
  205. end
  206. env['AWS_ACCESS_KEY_ID'] = prompt.ask('Minio access key:') do |q|
  207. q.required true
  208. q.modify :strip
  209. end
  210. env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('Minio secret key:') do |q|
  211. q.required true
  212. q.modify :strip
  213. end
  214. when 'Google Cloud Storage'
  215. env['S3_ENABLED'] = 'true'
  216. env['S3_PROTOCOL'] = 'https'
  217. env['S3_HOSTNAME'] = 'storage.googleapis.com'
  218. env['S3_ENDPOINT'] = 'https://storage.googleapis.com'
  219. env['S3_MULTIPART_THRESHOLD'] = 50.megabytes
  220. env['S3_BUCKET'] = prompt.ask('GCS bucket name:') do |q|
  221. q.required true
  222. q.default "files.#{env['LOCAL_DOMAIN']}"
  223. q.modify :strip
  224. end
  225. env['S3_REGION'] = prompt.ask('GCS region:') do |q|
  226. q.required true
  227. q.default 'us-west1'
  228. q.modify :strip
  229. end
  230. env['AWS_ACCESS_KEY_ID'] = prompt.ask('GCS access key:') do |q|
  231. q.required true
  232. q.modify :strip
  233. end
  234. env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('GCS secret key:') do |q|
  235. q.required true
  236. q.modify :strip
  237. end
  238. end
  239. if prompt.yes?('Do you want to access the uploaded files from your own domain?')
  240. env['S3_ALIAS_HOST'] = prompt.ask('Domain for uploaded files:') do |q|
  241. q.required true
  242. q.default "files.#{env['LOCAL_DOMAIN']}"
  243. q.modify :strip
  244. end
  245. end
  246. end
  247. prompt.say "\n"
  248. loop do
  249. if prompt.yes?('Do you want to send e-mails from localhost?', default: false)
  250. env['SMTP_SERVER'] = 'localhost'
  251. env['SMTP_PORT'] = 25
  252. env['SMTP_AUTH_METHOD'] = 'none'
  253. env['SMTP_OPENSSL_VERIFY_MODE'] = 'none'
  254. env['SMTP_ENABLE_STARTTLS'] = 'auto'
  255. else
  256. env['SMTP_SERVER'] = prompt.ask('SMTP server:') do |q|
  257. q.required true
  258. q.default 'smtp.mailgun.org'
  259. q.modify :strip
  260. end
  261. env['SMTP_PORT'] = prompt.ask('SMTP port:') do |q|
  262. q.required true
  263. q.default 587
  264. q.convert :int
  265. end
  266. env['SMTP_LOGIN'] = prompt.ask('SMTP username:') do |q|
  267. q.modify :strip
  268. end
  269. env['SMTP_PASSWORD'] = prompt.ask('SMTP password:') do |q|
  270. q.echo false
  271. end
  272. env['SMTP_AUTH_METHOD'] = prompt.ask('SMTP authentication:') do |q|
  273. q.required
  274. q.default 'plain'
  275. q.modify :strip
  276. end
  277. env['SMTP_OPENSSL_VERIFY_MODE'] = prompt.select('SMTP OpenSSL verify mode:', %w(none peer client_once fail_if_no_peer_cert))
  278. env['SMTP_ENABLE_STARTTLS'] = prompt.select('Enable STARTTLS:', %w(auto always never))
  279. end
  280. env['SMTP_FROM_ADDRESS'] = prompt.ask('E-mail address to send e-mails "from":') do |q|
  281. q.required true
  282. q.default "Mastodon <notifications@#{env['LOCAL_DOMAIN']}>"
  283. q.modify :strip
  284. end
  285. break unless prompt.yes?('Send a test e-mail with this configuration right now?')
  286. send_to = prompt.ask('Send test e-mail to:', required: true)
  287. begin
  288. enable_starttls = nil
  289. enable_starttls_auto = nil
  290. case env['SMTP_ENABLE_STARTTLS']
  291. when 'always'
  292. enable_starttls = true
  293. when 'never'
  294. enable_starttls = false
  295. when 'auto'
  296. enable_starttls_auto = true
  297. else
  298. enable_starttls_auto = env['SMTP_ENABLE_STARTTLS_AUTO'] != 'false'
  299. end
  300. ActionMailer::Base.smtp_settings = {
  301. port: env['SMTP_PORT'],
  302. address: env['SMTP_SERVER'],
  303. user_name: env['SMTP_LOGIN'].presence,
  304. password: env['SMTP_PASSWORD'].presence,
  305. domain: env['LOCAL_DOMAIN'],
  306. authentication: env['SMTP_AUTH_METHOD'] == 'none' ? nil : env['SMTP_AUTH_METHOD'] || :plain,
  307. openssl_verify_mode: env['SMTP_OPENSSL_VERIFY_MODE'],
  308. enable_starttls: enable_starttls,
  309. enable_starttls_auto: enable_starttls_auto,
  310. }
  311. ActionMailer::Base.default_options = {
  312. from: env['SMTP_FROM_ADDRESS'],
  313. }
  314. mail = ActionMailer::Base.new.mail to: send_to, subject: 'Test', body: 'Mastodon SMTP configuration works!'
  315. mail.deliver
  316. break
  317. rescue StandardError => e
  318. prompt.error 'E-mail could not be sent with this configuration, try again.'
  319. prompt.error e.message
  320. break unless prompt.yes?('Try again?')
  321. end
  322. end
  323. prompt.say "\n"
  324. prompt.say 'This configuration will be written to .env.production'
  325. if prompt.yes?('Save configuration?')
  326. incompatible_syntax = false
  327. env_contents = env.each_pair.map do |key, value|
  328. if value.is_a?(String) && value =~ /[\s\#\\"]/
  329. incompatible_syntax = true
  330. if value =~ /[']/
  331. value = value.to_s.gsub(/[\\"\$]/) { |x| "\\#{x}" }
  332. "#{key}=\"#{value}\""
  333. else
  334. "#{key}='#{value}'"
  335. end
  336. else
  337. "#{key}=#{value}"
  338. end
  339. end.join("\n")
  340. generated_header = "# Generated with mastodon:setup on #{Time.now.utc}\n\n".dup
  341. if incompatible_syntax
  342. generated_header << "# Some variables in this file will be interpreted differently whether you are\n"
  343. generated_header << "# using docker-compose or not.\n\n"
  344. end
  345. File.write(Rails.root.join('.env.production'), "#{generated_header}#{env_contents}\n")
  346. if using_docker
  347. prompt.ok 'Below is your configuration, save it to an .env.production file outside Docker:'
  348. prompt.say "\n"
  349. prompt.say "#{generated_header}#{env.each_pair.map { |key, value| "#{key}=#{value}" }.join("\n")}"
  350. prompt.say "\n"
  351. prompt.ok 'It is also saved within this container so you can proceed with this wizard.'
  352. end
  353. prompt.say "\n"
  354. prompt.say 'Now that configuration is saved, the database schema must be loaded.'
  355. prompt.warn 'If the database already exists, this will erase its contents.'
  356. if prompt.yes?('Prepare the database now?')
  357. prompt.say 'Running `RAILS_ENV=production rails db:setup` ...'
  358. prompt.say "\n\n"
  359. if !system(env.transform_values(&:to_s).merge({ 'RAILS_ENV' => 'production', 'SAFETY_ASSURED' => '1' }), 'rails db:setup')
  360. prompt.error 'That failed! Perhaps your configuration is not right'
  361. else
  362. prompt.ok 'Done!'
  363. end
  364. end
  365. unless using_docker
  366. prompt.say "\n"
  367. prompt.say 'The final step is compiling CSS/JS assets.'
  368. prompt.say 'This may take a while and consume a lot of RAM.'
  369. if prompt.yes?('Compile the assets now?')
  370. prompt.say 'Running `RAILS_ENV=production rails assets:precompile` ...'
  371. prompt.say "\n\n"
  372. if !system(env.transform_values(&:to_s).merge({ 'RAILS_ENV' => 'production' }), 'rails assets:precompile')
  373. prompt.error 'That failed! Maybe you need swap space?'
  374. else
  375. prompt.say 'Done!'
  376. end
  377. end
  378. end
  379. prompt.say "\n"
  380. prompt.ok 'All done! You can now power on the Mastodon server 🐘'
  381. prompt.say "\n"
  382. if db_connection_works && prompt.yes?('Do you want to create an admin user straight away?')
  383. env.each_pair do |key, value|
  384. ENV[key] = value.to_s
  385. end
  386. require_relative '../../config/environment'
  387. disable_log_stdout!
  388. username = prompt.ask('Username:') do |q|
  389. q.required true
  390. q.default 'admin'
  391. q.validate(/\A[a-z0-9_]+\z/i)
  392. q.modify :strip
  393. end
  394. email = prompt.ask('E-mail:') do |q|
  395. q.required true
  396. q.modify :strip
  397. end
  398. password = SecureRandom.hex(16)
  399. owner_role = UserRole.find_by(name: 'Owner')
  400. user = User.new(email: email, password: password, confirmed_at: Time.now.utc, account_attributes: { username: username }, bypass_invite_request_check: true, role: owner_role)
  401. user.save(validate: false)
  402. Setting.site_contact_username = username
  403. prompt.ok "You can login with the password: #{password}"
  404. prompt.warn 'You can change your password once you login.'
  405. end
  406. else
  407. prompt.warn 'Nothing saved. Bye!'
  408. end
  409. rescue TTY::Reader::InputInterrupt
  410. prompt.ok 'Aborting. Bye!'
  411. end
  412. end
  413. namespace :webpush do
  414. desc 'Generate VAPID key'
  415. task :generate_vapid_key do
  416. vapid_key = Webpush.generate_key
  417. puts "VAPID_PRIVATE_KEY=#{vapid_key.private_key}"
  418. puts "VAPID_PUBLIC_KEY=#{vapid_key.public_key}"
  419. end
  420. end
  421. end
  422. def disable_log_stdout!
  423. dev_null = Logger.new('/dev/null')
  424. Rails.logger = dev_null
  425. ActiveRecord::Base.logger = dev_null
  426. HttpLog.configuration.logger = dev_null
  427. Paperclip.options[:log] = false
  428. end