From 743d628733fab989bfd17ffaafa4c0c19009cb64 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 14 May 2014 18:51:59 +0200 Subject: [PATCH] Islo w/basic MySQL, Redis and PostgreSQL support --- .gitignore | 2 + Gemfile | 3 + LICENSE | 20 ++++ README.md | 74 +++++++++++++ Rakefile | 0 bin/islo | 5 + islo.gemspec | 29 +++++ lib/islo.rb | 264 ++++++++++++++++++++++++++++++++++++++++++++ lib/islo/cli.rb | 91 +++++++++++++++ lib/islo/version.rb | 3 + 10 files changed, 491 insertions(+) create mode 100644 .gitignore create mode 100644 Gemfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Rakefile create mode 100755 bin/islo create mode 100644 islo.gemspec create mode 100644 lib/islo.rb create mode 100644 lib/islo/cli.rb create mode 100644 lib/islo/version.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cec3cb5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.gem +Gemfile.lock diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..fa75df1 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gemspec diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d8a9d9c --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2014 Loic Nageleisen + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..808ff53 --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# Islo - Self-contained apps + +Make apps completely self-contained by abstracting service process settings and execution. + +## Quick, show me how to use my favorite daemon! + +First, install Islo: + +``` +$ gem install islo +``` + +Then, a nice example might be worth a thousand words, so here goes: + +### MySQL or MariaDB + +``` +$ islo mysql_install_db # creates database in db/mysql +$ islo mysqld # starts server without daemonizing[^1] +$ islo mysql # connects to running server via unix socket in tmp/sockets +``` + +### Redis + +``` +$ islo redis-init # creates directory in db/redis +$ islo redis-server # starts server without daemonizing[^1] +$ islo redis-cli # connects to server via unix socket in tmp/sockets +``` + +### PostgreSQL + +``` +$ islo initdb # creates directory in db/postgres +$ islo postgres # starts server without daemonizing[^1] +$ islo psql # connects to server via unix socket in tmp/sockets +``` + +[^1]: Best used in a [Procfile](https://github.com/ddollar/foreman) + +## What's more to know? + +- Additional arguments are passed to the command. + + Run `islo --help` for details. + +- Servers will listen only on unix sockets, TCP will be disabled. + + This saves headaches when you have to handle multiple projects, and thus + conflicting ports. Also, it's too easy to forget not to listen for the world. + +## My service is installed in a non-standard location/I want to use different versions in different projects + +Configuration is a pending item, which will make locations selectable. + +## I don't like how it assumes a Rails project layout + +Configuration is a pending item, which will help set relevant paths. + +## I've got a super service you don't seem to know about + +Some configuration may help you soon. Also, contributions are welcome. + +## I can't be bothered/always forget to type *islo* before my commands every single time! + +Look soon enough under `support` for a few optional helpers for your favorite shell. + +## I want to contribute. How? + +Great! Write specs, have them all pass, respect rubocop, rebase on master and make your PR. + +## License + +MIT, see [LICENSE](LICENSE). diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..e69de29 diff --git a/bin/islo b/bin/islo new file mode 100755 index 0000000..cf5badd --- /dev/null +++ b/bin/islo @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby + +require 'islo/cli' + +Islo::CLI.start diff --git a/islo.gemspec b/islo.gemspec new file mode 100644 index 0000000..1a09c00 --- /dev/null +++ b/islo.gemspec @@ -0,0 +1,29 @@ +# -*- encoding: utf-8 -*- +$LOAD_PATH.push File.expand_path('../lib', __FILE__) +require 'islo/version' + +Gem::Specification.new do |s| + s.name = 'islo' + s.version = Islo::VERSION + s.authors = ['Loic Nageleisen'] + s.email = ['loic.nageleisen@gmail.com'] + s.homepage = 'http://github.com/lloeki/islo' + s.summary = %q(Self-contained apps) + s.description = <<-EOT + Makes app completely self-contained by abstracting + service process settings and execution + EOT + s.license = 'MIT' + s.files = `git ls-files`.split("\n") + s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") + s.executables = `git ls-files -- bin/*`.split("\n") + .map { |f| File.basename(f) } + s.require_paths = ['lib'] + + s.add_dependency 'rainbow', '~> 2.0' + s.add_dependency 'slop' + + s.add_development_dependency 'rspec', '~> 2.14' + s.add_development_dependency 'rake', '~> 10.3' + s.add_development_dependency 'pry' +end diff --git a/lib/islo.rb b/lib/islo.rb new file mode 100644 index 0000000..cce8d28 --- /dev/null +++ b/lib/islo.rb @@ -0,0 +1,264 @@ +# Islo - application isolator +module Islo + # Generic command execution + class Command + class Error < StandardError; end + + attr_reader :title, :wd + attr_reader :command, :args + + def initialize(args, title: nil, wd: nil) + @command = args.shift + @args = args + @title = title unless title.nil? || title.empty? + @wd = Pathname.new(wd || Dir.pwd) + end + + def title? + !title.nil? + end + + def exec + Dir.chdir(wd.to_s) + Kernel.exec(*args_for_exec) + rescue SystemCallError => e + raise Command::Error, e.message + end + + private + + def args_for_exec + command = (title? ? [@command, title] : @command) + args = self.args + + [command] + args + end + end + + class << self + def commands + @commands ||= {} + end + + def register(command) + commands[command.name.to_sym] = command + end + + def command(args, options = {}) + name = File.basename(args[0]).to_sym + (commands[name] || Command).new(args, options) + end + end + + # MySQL support + module Mysql + # MySQL client + class Client < Command + def self.name + :mysql + end + + def args + %W( + --socket=#{wd}/tmp/sockets/mysql.sock + -uroot + ) + super + end + + Islo.register(self) + end + + # MySQL server + class Server < Command + def self.name + :mysqld + end + + def args + %W( + --no-defaults + --datadir=#{wd}/db/mysql + --pid-file=#{wd}/tmp/pids/mysqld.pid + --socket=#{wd}/tmp/sockets/mysql.sock + --skip-networking + ) + super + end + + Islo.register(self) + end + + # MySQL initializer + class Init < Command + def self.name + :mysql_install_db + end + + def args + %W( + --no-defaults + --basedir=#{Mysql.basedir} + --datadir=#{wd}/db/mysql + --pid-file=#{wd}/tmp/pids/mysqld.pid + ) + super + end + + Islo.register(self) + end + + def self.basedir + '/usr/local' + end + end + + # Redis support + module Redis + # Redis client + class Client < Command + def self.name + :'redis-cli' + end + + def args + %w(-s redis.sock) + end + + # Change working directory (makes for a nicer prompt) + def wd + super + 'tmp/sockets' + end + + Islo.register(self) + end + + # Redis server + class Server < Command + def self.name + :'redis-server' + end + + def args + %W(#{wd}/db/redis/redis.conf) + end + + Islo.register(self) + end + + # Redis initializer + # + # Creates a minimal configuration because redis-server doesn't accept + # arguments allowing for paths to be set. + class Init < Command + def self.name + :'redis-init' + end + + def exec + FileUtils.mkdir_p(wd + 'db/redis') + + File.open(wd + 'db/redis/redis.conf', 'w') do |f| + f << template.gsub('${WORKING_DIR}', wd.to_s) + end + rescue SystemCallError => e + raise Command::Error, e.message + end + + private + + # rubocop:disable MethodLength,LineLength + def template + <<-EOT.gsub(/^ +/, '') + daemonize no + pidfile ${WORKING_DIR}/pids/redis.pid + port 0 + bind 127.0.0.1 + unixsocket ${WORKING_DIR}/tmp/sockets/redis.sock + unixsocketperm 700 + timeout 0 + tcp-keepalive 0 + loglevel notice + databases 1 + save 900 1 + save 300 10 + save 60 10000 + stop-writes-on-bgsave-error yes + rdbcompression yes + rdbchecksum yes + dbfilename dump.rdb + dir ${WORKING_DIR}/db/redis + slave-serve-stale-data yes + slave-read-only yes + repl-disable-tcp-nodelay no + slave-priority 100 + appendonly yes + appendfsync everysec + no-appendfsync-on-rewrite no + auto-aof-rewrite-percentage 100 + auto-aof-rewrite-min-size 64mb + lua-time-limit 5000 + slowlog-log-slower-than 10000 + slowlog-max-len 128 + hash-max-ziplist-entries 512 + hash-max-ziplist-value 64 + list-max-ziplist-entries 512 + list-max-ziplist-value 64 + set-max-intset-entries 512 + zset-max-ziplist-entries 128 + zset-max-ziplist-value 64 + activerehashing yes + client-output-buffer-limit normal 0 0 0 + client-output-buffer-limit slave 256mb 64mb 60 + client-output-buffer-limit pubsub 32mb 8mb 60 + EOT + end + + Islo.register(self) + end + end + + # PostgreSQL support + module Postgres + # PostgreSQL client + class Client < Command + def self.name + :psql + end + + def args + %W(--host=#{wd}/tmp/sockets) + super + end + + Islo.register(self) + end + + # PostgreSQL server + class Server < Command + def self.name + :postgres + end + + def args + %W( + -D #{wd}/db/postgres + -k #{wd}/tmp/sockets + ) + ['-h', ''] + super + end + + Islo.register(self) + end + + # PostgreSQL initializer + class Init < Command + def self.name + :initdb + end + + def args + %W( + -D #{wd}/db/postgres + ) + super + end + + Islo.register(self) + end + end +end diff --git a/lib/islo/cli.rb b/lib/islo/cli.rb new file mode 100644 index 0000000..9e01b26 --- /dev/null +++ b/lib/islo/cli.rb @@ -0,0 +1,91 @@ +require 'slop' +require 'islo' +require 'rainbow/ext/string' + +module Islo + # Handle and run the command line interface + class CLI + NAME = File.basename($PROGRAM_NAME) + + # See sysexits(3) + RC = { ok: 0, + error: 1, + usage: 64, + data: 65, + noinput: 66, + nouser: 67, + nohost: 68, + unavailable: 69, + software: 70, + oserr: 71, + osfile: 72, + cantcreat: 73, + ioerr: 74, + tempfail: 75, + protocol: 76, + noperm: 77, + config: 78 } + + # Shortcut for Islo::CLI.new.start + def self.start + new.start + end + + attr_reader :args + + def opts + @opts.to_hash + end + + def initialize(args = ARGV) + @args, @opts = parse(args) + rescue Slop::InvalidOptionError => e + die(:usage, e.message) + rescue Slop::MissingArgumentError => e + die(:usage, e.message) + end + + # Start execution based on options + def start + Islo.command(args, title: opts[:title]).exec + rescue Islo::Command::Error => e + die(:oserr, e.message) + end + + private + + # Stop with a popping message and exit with a return code + def die(rc, message) + $stderr.puts('Error: '.color(:red) << message) + exit(rc.is_a?(Symbol) ? RC[rc] : rc) + end + + # Stop with a nice message + def bye(message) + $stdout.puts message + exit + end + + def version_string + "#{NAME} v#{Islo::VERSION}" + end + + # Parse arguments and set options + # rubocop:disable MethodLength + def parse(args) + args = args.dup + + opts = Slop.parse!(args, strict: true, help: true) do + banner "Usage: #{NAME} [options] [--] command arguments" + + on 't', 'title=', 'String displayed in process list', argument: true + on 'd', 'directory=', 'Change working directory', argument: true + on 'v', 'version', 'Print the version', -> { bye(version_string) } + end + + fail Slop::MissingArgumentError, 'missing an argument' if args.empty? + + [args, opts] + end + end +end diff --git a/lib/islo/version.rb b/lib/islo/version.rb new file mode 100644 index 0000000..596761e --- /dev/null +++ b/lib/islo/version.rb @@ -0,0 +1,3 @@ +module Islo + VERSION = '0.1.0' +end