diff options
author | Eugen Rochko <eugen@zeonfederated.com> | 2018-02-11 18:40:57 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-02-11 18:40:57 +0100 |
commit | cf36d184f41b5bfc0c63d6c8409b05cca2eb67ee (patch) | |
tree | c3988e1788b8413688071b30c32dad6affcbc02e /lib | |
parent | 718802a05dfb3211d758513daf6070ffa22751dd (diff) |
Interactive `rake mastodon:setup` task (#6451)
* Add better CLI prompt * Add rake mastodon:setup interactive wizard * Test db/redis/smtp configurations and add admin user at the end * Test database connection even when database does not exist yet
Diffstat (limited to 'lib')
-rw-r--r-- | lib/tasks/mastodon.rake | 410 |
1 files changed, 387 insertions, 23 deletions
diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index 486c035de..e144621e5 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -4,6 +4,362 @@ require 'optparse' require 'colorize' namespace :mastodon do + desc 'Configure the instance for production use' + task :setup do + prompt = TTY::Prompt.new + env = {} + + begin + prompt.say('Your instance is identified by its domain name. Changing it afterward will break things.') + env['LOCAL_DOMAIN'] = prompt.ask('Domain name:') do |q| + q.required true + q.modify :strip + q.validate(/\A[a-z0-9\.\-]+\z/i) + q.messages[:valid?] = 'Invalid domain. If you intend to use unicode characters, enter punycode here' + end + + prompt.say "\n" + + prompt.say('Single user mode disables registrations and redirects the landing page to your public profile.') + env['SINGLE_USER_MODE'] = prompt.yes?('Do you want to enable single user mode?', default: false) + + %w(SECRET_KEY_BASE PAPERCLIP_SECRET OTP_SECRET).each do |key| + env[key] = SecureRandom.hex(64) + end + + vapid_key = Webpush.generate_key + + env['VAPID_PRIVATE_KEY'] = vapid_key.private_key + env['VAPID_PUBLIC_KEY'] = vapid_key.public_key + + prompt.say "\n" + + using_docker = prompt.yes?('Are you using Docker to run Mastodon?') + db_connection_works = false + + prompt.say "\n" + + loop do + env['DB_HOST'] = prompt.ask('PostgreSQL host:') do |q| + q.required true + q.default using_docker ? 'db' : '/var/run/postgresql' + q.modify :strip + end + + env['DB_PORT'] = prompt.ask('PostgreSQL port:') do |q| + q.required true + q.default 5432 + q.convert :int + end + + env['DB_NAME'] = prompt.ask('Name of PostgreSQL database:') do |q| + q.required true + q.default using_docker ? 'postgres' : 'mastodon_production' + q.modify :strip + end + + env['DB_USER'] = prompt.ask('Name of PostgreSQL user:') do |q| + q.required true + q.default using_docker ? 'postgres' : 'mastodon' + q.modify :strip + end + + env['DB_PASS'] = prompt.ask('Password of PostgreSQL user:') do |q| + q.echo false + end + + # The chosen database may not exist yet. Connect to default database + # to avoid "database does not exist" error. + db_options = { + adapter: :postgresql, + database: 'postgres', + host: env['DB_HOST'], + port: env['DB_PORT'], + user: env['DB_USER'], + password: env['DB_PASS'], + } + + begin + ActiveRecord::Base.establish_connection(db_options) + ActiveRecord::Base.connection + prompt.ok 'Database configuration works! 🎆' + db_connection_works = true + break + rescue StandardError => e + prompt.error 'Database connection could not be established with this configuration, try again.' + prompt.error e.message + break unless prompt.yes?('Try again?') + end + end + + prompt.say "\n" + + loop do + env['REDIS_HOST'] = prompt.ask('Redis host:') do |q| + q.required true + q.default using_docker ? 'redis' : 'localhost' + q.modify :strip + end + + env['REDIS_PORT'] = prompt.ask('Redis port:') do |q| + q.required true + q.default 6379 + q.convert :int + end + + redis_options = { + host: env['REDIS_HOST'], + port: env['REDIS_PORT'], + driver: :hiredis, + } + + begin + redis = Redis.new(redis_options) + redis.ping + prompt.ok 'Redis configuration works! 🎆' + break + rescue StandardError => e + prompt.error 'Redis connection could not be established with this configuration, try again.' + prompt.error e.message + break unless prompt.yes?('Try again?') + end + end + + prompt.say "\n" + + if prompt.yes?('Do you want to store uploaded files on the cloud?', default: false) + case prompt.select('Provider', ['Amazon S3', 'Wasabi', 'Minio']) + when 'Amazon S3' + env['S3_ENABLED'] = 'true' + env['S3_PROTOCOL'] = 'https' + + env['S3_BUCKET'] = prompt.ask('S3 bucket name:') do |q| + q.required true + q.default "files.#{env['LOCAL_DOMAIN']}" + q.modify :strip + end + + env['S3_REGION'] = prompt.ask('S3 region:') do |q| + q.required true + q.default 'us-east-1' + q.modify :strip + end + + env['S3_HOSTNAME'] = prompt.ask('S3 hostname:') do |q| + q.required true + q.default 's3-us-east-1.amazonaws.com' + q.modify :strip + end + + env['AWS_ACCESS_KEY_ID'] = prompt.ask('S3 access key:') do |q| + q.required true + q.modify :strip + end + + env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('S3 secret key:') do |q| + q.required true + q.modify :strip + end + when 'Wasabi' + env['S3_ENABLED'] = 'true' + env['S3_PROTOCOL'] = 'https' + env['S3_REGION'] = 'us-east-1' + env['S3_HOSTNAME'] = 's3.wasabisys.com' + env['S3_ENDPOINT'] = 'https://s3.wasabisys.com/' + + env['S3_BUCKET'] = prompt.ask('Wasabi bucket name:') do |q| + q.required true + q.default "files.#{env['LOCAL_DOMAIN']}" + q.modify :strip + end + + env['AWS_ACCESS_KEY_ID'] = prompt.ask('Wasabi access key:') do |q| + q.required true + q.modify :strip + end + + env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('Wasabi secret key:') do |q| + q.required true + q.modify :strip + end + when 'Minio' + env['S3_ENABLED'] = 'true' + env['S3_PROTOCOL'] = 'https' + env['S3_REGION'] = 'us-east-1' + + env['S3_ENDPOINT'] = prompt.ask('Minio endpoint URL:') do |q| + q.required true + q.modify :strip + end + + env['S3_PROTOCOL'] = env['S3_ENDPOINT'].start_with?('https') ? 'https' : 'http' + env['S3_HOSTNAME'] = env['S3_ENDPOINT'].gsub(/\Ahttps?:\/\//, '') + + env['S3_BUCKET'] = prompt.ask('Minio bucket name:') do |q| + q.required true + q.default "files.#{env['LOCAL_DOMAIN']}" + q.modify :strip + end + + env['AWS_ACCESS_KEY_ID'] = prompt.ask('Minio access key:') do |q| + q.required true + q.modify :strip + end + + env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('Minio secret key:') do |q| + q.required true + q.modify :strip + end + end + + if prompt.yes?('Do you want to access the uploaded files from your own domain?') + env['S3_CLOUDFRONT_HOST'] = prompt.ask('Domain for uploaded files:') do |q| + q.required true + q.default "files.#{env['LOCAL_DOMAIN']}" + q.modify :strip + end + end + end + + prompt.say "\n" + + loop do + env['SMTP_SERVER'] = prompt.ask('SMTP server:') do |q| + q.required true + q.default 'smtp.mailgun.org' + q.modify :strip + end + + env['SMTP_PORT'] = prompt.ask('SMTP port:') do |q| + q.required true + q.default 587 + q.convert :int + end + + env['SMTP_LOGIN'] = prompt.ask('SMTP username:') do |q| + q.modify :strip + end + + env['SMTP_PASSWORD'] = prompt.ask('SMTP password:') do |q| + q.echo false + end + + env['SMTP_FROM_ADDRESS'] = prompt.ask('E-mail address to send e-mails "from":') do |q| + q.required true + q.default "Mastodon <notifications@#{env['LOCAL_DOMAIN']}>" + q.modify :strip + end + + break unless prompt.yes?('Send a test e-mail with this configuration right now?') + + send_to = prompt.ask('Send test e-mail to:', required: true) + + begin + ActionMailer::Base.smtp_settings = { + :port => env['SMTP_PORT'], + :address => env['SMTP_SERVER'], + :user_name => env['SMTP_LOGIN'].presence, + :password => env['SMTP_PASSWORD'].presence, + :domain => env['LOCAL_DOMAIN'], + :authentication => :plain, + :enable_starttls_auto => true, + } + + ActionMailer::Base.default_options = { + from: env['SMTP_FROM_ADDRESS'], + } + + mail = ActionMailer::Base.new.mail to: send_to, subject: 'Test', body: 'Mastodon SMTP configuration works!' + mail.deliver + rescue StandardError => e + prompt.error 'E-mail could not be sent with this configuration, try again.' + prompt.error e.message + break unless prompt.yes?('Try again?') + end + end + + prompt.say "\n" + prompt.say 'This configuration will be written to .env.production' + + if prompt.yes?('Save configuration?') + cmd = TTY::Command.new(printer: :quiet) + + File.write(Rails.root.join('.env.production'), "# Generated with mastodon:setup on #{Time.now.utc}\n\n" + env.each_pair.map { |key, value| "#{key}=#{value}" }.join("\n") + "\n") + + prompt.say "\n" + prompt.say 'Now that configuration is saved, the database schema must be loaded.' + prompt.warn 'If the database already exists, this will erase its contents.' + + if prompt.yes?('Prepare the database now?') + prompt.say 'Running `RAILS_ENV=production rails db:setup` ...' + prompt.say "\n" + + if cmd.run!({ RAILS_ENV: 'production' }, :rails, 'db:setup').failure? + prompt.say "\n" + prompt.error 'That failed! Perhaps your configuration is not right' + else + prompt.say "\n" + prompt.ok 'Done!' + end + end + + prompt.say "\n" + prompt.say 'The final step is compiling CSS/JS assets.' + prompt.say 'This may take a while and consume a lot of RAM.' + + if prompt.yes?('Compile the assets now?') + prompt.say 'Running `RAILS_ENV=production rails assets:precompile` ...' + prompt.say "\n" + + if cmd.run!({ RAILS_ENV: 'production' }, :rails, 'assets:precompile').failure? + prompt.say "\n" + prompt.error 'That failed! Maybe you need swap space?' + else + prompt.say "\n" + prompt.say 'Done!' + end + end + + prompt.say "\n" + prompt.ok 'All done! You can now power on the Mastodon server 🐘' + prompt.say "\n" + + if db_connection_works && prompt.yes?('Do you want to create an admin user straight away?') + env.each_pair do |key, value| + ENV[key] = value.to_s + end + + require_relative '../../config/environment' + disable_log_stdout! + + username = prompt.ask('Username:') do |q| + q.required true + q.default 'admin' + q.validate(/\A[a-z0-9_]+\z/i) + q.modify :strip + end + + email = prompt.ask('E-mail:') do |q| + q.required true + q.modify :strip + end + + password = SecureRandom.hex(16) + + user = User.new(admin: true, email: email, password: password, confirmed_at: Time.now.utc, account_attributes: { username: username }) + user.save(validate: false) + + prompt.ok "You can login with the password: #{password}" + prompt.warn 'You can change your password once you login.' + end + else + prompt.warn 'Nothing saved. Bye!' + end + rescue TTY::Reader::InputInterrupt + prompt.ok 'Aborting. Bye!' + end + end + desc 'Execute daily tasks (deprecated)' task :daily do # No-op @@ -67,32 +423,40 @@ namespace :mastodon do desc 'Add a user by providing their email, username and initial password.' \ 'The user will receive a confirmation email, then they must reset their password before logging in.' task add_user: :environment do - print 'Enter email: ' - email = STDIN.gets.chomp - - print 'Enter username: ' - username = STDIN.gets.chomp - - print 'Create user and send them confirmation mail [y/N]: ' - confirm = STDIN.gets.chomp - puts - - if confirm.casecmp('y').zero? - password = SecureRandom.hex - user = User.new(email: email, password: password, account_attributes: { username: username }) - if user.save - puts 'User added and confirmation mail sent to user\'s email address.' - puts "Here is the random password generated for the user: #{password}" - else - puts 'Following errors occured while creating new user:' - user.errors.each do |key, val| - puts "#{key}: #{val}" + disable_log_stdout! + + prompt = TTY::Prompt.new + + begin + email = prompt.ask('E-mail:', required: true) do |q| + q.modify :strip + end + + username = prompt.ask('Username:', required: true) do |q| + q.modify :strip + end + + role = prompt.select('Role:', %w(user moderator admin)) + + if prompt.yes?('Proceed to create the user?') + user = User.new(email: email, password: SecureRandom.hex, admin: role == 'admin', moderator: role == 'moderator', account_attributes: { username: username }) + + if user.save + prompt.ok 'User created and confirmation mail sent to the user\'s email address.' + prompt.ok "Here is the random password generated for the user: #{password}" + else + prompt.warn 'User was not created because of the following errors:' + + user.errors.each do |key, val| + prompt.error "#{key}: #{val}" + end end + else + prompt.ok 'Aborting. Bye!' end - else - puts 'Aborted by user.' + rescue TTY::Reader::InputInterrupt + prompt.ok 'Aborting. Bye!' end - puts end namespace :media do |