vps 0.1.1
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/.gitignore +8 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.md +107 -0
- data/Rakefile +10 -0
- data/VERSION +1 -0
- data/bin/vps +10 -0
- data/config/services.yml +63 -0
- data/lib/vps.rb +88 -0
- data/lib/vps/cli.rb +61 -0
- data/lib/vps/cli/domain.rb +59 -0
- data/lib/vps/cli/playbook.rb +110 -0
- data/lib/vps/cli/playbook/state.rb +170 -0
- data/lib/vps/cli/playbook/tasks.rb +262 -0
- data/lib/vps/cli/service.rb +96 -0
- data/lib/vps/cli/upstream.rb +86 -0
- data/lib/vps/core_ext/string.rb +33 -0
- data/lib/vps/version.rb +7 -0
- data/playbooks/deploy.yml +43 -0
- data/playbooks/deploy/docker.yml +96 -0
- data/playbooks/init.yml +12 -0
- data/playbooks/init/ubuntu-18.04.yml +110 -0
- data/playbooks/install.yml +18 -0
- data/playbooks/install/docker/ubuntu-18.04.yml +35 -0
- data/script/console +7 -0
- data/templates/docker/data/nginx/app.conf.erb +52 -0
- data/templates/docker/docker-compose.yml.erb +56 -0
- data/templates/docker/upstream/Dockerfile.phoenix.erb +13 -0
- data/templates/docker/upstream/Dockerfile.plug.erb +13 -0
- data/templates/docker/upstream/Dockerfile.rack.erb +15 -0
- data/templates/docker/upstream/Dockerfile.rails.erb +15 -0
- data/templates/docker/upstream/init-letsencrypt.sh.erb +76 -0
- data/test/test_helper.rb +12 -0
- data/test/test_helper/coverage.rb +8 -0
- data/test/unit/test_version.rb +15 -0
- data/vps.gemspec +29 -0
- metadata +213 -0
| @@ -0,0 +1,110 @@ | |
| 1 | 
            +
            require "vps/cli/playbook/state"
         | 
| 2 | 
            +
            require "vps/cli/playbook/tasks"
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module VPS
         | 
| 5 | 
            +
              class CLI < Thor
         | 
| 6 | 
            +
                class Playbook
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  class NotFoundError < VPS::CLI::Error; end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  YML = ".yml"
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  attr_reader :command
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def self.all
         | 
| 15 | 
            +
                    Dir["#{VPS::PLAYBOOKS}/*#{YML}"].collect do |playbook|
         | 
| 16 | 
            +
                      command = File.basename(playbook, YML)
         | 
| 17 | 
            +
                      new(playbook, command)
         | 
| 18 | 
            +
                    end
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  def self.run(playbook, state)
         | 
| 22 | 
            +
                    playbook = File.expand_path(playbook, VPS::PLAYBOOKS)
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    if File.directory?(playbook)
         | 
| 25 | 
            +
                      playbook += "/#{state.server_version}"
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
                    unless File.extname(playbook) == YML
         | 
| 28 | 
            +
                      playbook += YML
         | 
| 29 | 
            +
                    end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                    new(playbook).run(state)
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  def initialize(playbook, command = nil)
         | 
| 35 | 
            +
                    unless File.exists?(playbook)
         | 
| 36 | 
            +
                      raise NotFoundError, "Could not find playbook #{playbook.inspect}"
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    @playbook = {"constants" => {}}.merge(YAML.load_file(playbook))
         | 
| 40 | 
            +
                    unless (playbooks = Dir[playbook.gsub(/\.\w+$/, "/*")].collect{|yml| File.basename(yml, ".yml")}).empty?
         | 
| 41 | 
            +
                      @playbook["constants"]["playbooks"] = playbooks
         | 
| 42 | 
            +
                    end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                    @command = command
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  def description
         | 
| 48 | 
            +
                    playbook["description"]
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  def usage
         | 
| 52 | 
            +
                    playbook["usage"] || arguments.collect(&:upcase).unshift(@command).join(" ")
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  def arguments
         | 
| 56 | 
            +
                    playbook["arguments"] || []
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  def constants
         | 
| 60 | 
            +
                    playbook["constants"] || {}
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  def options
         | 
| 64 | 
            +
                    options = playbook["options"] || {}
         | 
| 65 | 
            +
                    options[%w(-d --dry-run)] = :boolean
         | 
| 66 | 
            +
                    options
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                  def tasks
         | 
| 70 | 
            +
                    @tasks ||= begin
         | 
| 71 | 
            +
                      tasks = [playbook["tasks"]].flatten.compact
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                      if requires_confirmation?
         | 
| 74 | 
            +
                        tasks.unshift({
         | 
| 75 | 
            +
                          "task" => :confirm,
         | 
| 76 | 
            +
                          "question" => playbook["confirm"],
         | 
| 77 | 
            +
                          "indent" => false,
         | 
| 78 | 
            +
                          "n" => :abort
         | 
| 79 | 
            +
                        })
         | 
| 80 | 
            +
                      end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                      Tasks.new(tasks)
         | 
| 83 | 
            +
                    end
         | 
| 84 | 
            +
                  end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                  def run!(args, options)
         | 
| 87 | 
            +
                    hash = Hash[arguments.zip(args)]
         | 
| 88 | 
            +
                    state = State.new(hash.merge(options))
         | 
| 89 | 
            +
                    run(state)
         | 
| 90 | 
            +
                  end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                  def run(state)
         | 
| 93 | 
            +
                    state.scope(constants) do
         | 
| 94 | 
            +
                      tasks.run(state)
         | 
| 95 | 
            +
                    end
         | 
| 96 | 
            +
                  end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                private
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                  def playbook
         | 
| 101 | 
            +
                    @playbook
         | 
| 102 | 
            +
                  end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                  def requires_confirmation?
         | 
| 105 | 
            +
                    playbook["confirm"].to_s.strip != ""
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                end
         | 
| 109 | 
            +
              end
         | 
| 110 | 
            +
            end
         | 
| @@ -0,0 +1,170 @@ | |
| 1 | 
            +
            module VPS
         | 
| 2 | 
            +
              class CLI < Thor
         | 
| 3 | 
            +
                class Playbook
         | 
| 4 | 
            +
                  class State
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                    class AuthenticationFailedError < VPS::CLI::Error; end
         | 
| 7 | 
            +
                    class SSHMock
         | 
| 8 | 
            +
                      def initialize
         | 
| 9 | 
            +
                        puts "🏄♀️ ~> ".gray + "Mocking SSH connection with Ubuntu 18.04.2 LTS server".cyan
         | 
| 10 | 
            +
                      end
         | 
| 11 | 
            +
                      def exec!(command)
         | 
| 12 | 
            +
                        case command
         | 
| 13 | 
            +
                        when "cat /etc/lsb-release"
         | 
| 14 | 
            +
                          <<-LSB
         | 
| 15 | 
            +
                          DISTRIB_ID=Ubuntu
         | 
| 16 | 
            +
                          DISTRIB_RELEASE=18.04
         | 
| 17 | 
            +
                          DISTRIB_CODENAME=bionic
         | 
| 18 | 
            +
                          DISTRIB_DESCRIPTION="Ubuntu 18.04.2 LTS"
         | 
| 19 | 
            +
                          LSB
         | 
| 20 | 
            +
                        when "pwd"
         | 
| 21 | 
            +
                          "/home/myapp"
         | 
| 22 | 
            +
                        else
         | 
| 23 | 
            +
                          raise "Encountered unexpected command: #{command}"
         | 
| 24 | 
            +
                        end
         | 
| 25 | 
            +
                      end
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    SERVER_VERSION = "SERVER_VERSION"
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    def initialize(hash = {})
         | 
| 31 | 
            +
                      @stack = [hash.with_indifferent_access]
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                    def dry_run?
         | 
| 35 | 
            +
                      !!fetch(:d)
         | 
| 36 | 
            +
                    end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    def scope(constants = {})
         | 
| 39 | 
            +
                      stack.unshift(constants.with_indifferent_access)
         | 
| 40 | 
            +
                      constants.keys.each do |key|
         | 
| 41 | 
            +
                        self[key] = resolve(self[key])
         | 
| 42 | 
            +
                      end
         | 
| 43 | 
            +
                      yield
         | 
| 44 | 
            +
                      stack.shift
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    def fetch(key, default = nil)
         | 
| 48 | 
            +
                      stack.each do |hash|
         | 
| 49 | 
            +
                        return hash[key] if hash.key?(key)
         | 
| 50 | 
            +
                      end
         | 
| 51 | 
            +
                      default
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    def [](path)
         | 
| 55 | 
            +
                      to_domain = !!(path = path.dup).gsub!("domain:", "") if path.is_a?(String)
         | 
| 56 | 
            +
                      path.to_s.split(".").inject(self) do |hash, key|
         | 
| 57 | 
            +
                        (hash || {}).fetch(key)
         | 
| 58 | 
            +
                      end.tap do |value|
         | 
| 59 | 
            +
                        if to_domain && value
         | 
| 60 | 
            +
                          if (domain = value[:domains].first)
         | 
| 61 | 
            +
                            return domain.gsub(/https?:\/\//, "")
         | 
| 62 | 
            +
                          end
         | 
| 63 | 
            +
                        end
         | 
| 64 | 
            +
                      end
         | 
| 65 | 
            +
                    end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                    def []=(key, value)
         | 
| 68 | 
            +
                      stack.first[key] = value
         | 
| 69 | 
            +
                    end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                    def to_binding(object = self)
         | 
| 72 | 
            +
                      case object
         | 
| 73 | 
            +
                      when State
         | 
| 74 | 
            +
                        keys = stack.collect(&:keys).flatten.uniq
         | 
| 75 | 
            +
                        keys.inject({state: object}) do |hash, key|
         | 
| 76 | 
            +
                          hash[key] = to_binding(self[key])
         | 
| 77 | 
            +
                          hash
         | 
| 78 | 
            +
                        end
         | 
| 79 | 
            +
                      when Hash
         | 
| 80 | 
            +
                        hash = object.inject({}) do |hash, (key, value)|
         | 
| 81 | 
            +
                          hash[key] = to_binding(resolve(value))
         | 
| 82 | 
            +
                          hash
         | 
| 83 | 
            +
                        end
         | 
| 84 | 
            +
                        OpenStruct.new(hash)
         | 
| 85 | 
            +
                      when Array
         | 
| 86 | 
            +
                        object.collect{|object| to_binding(object)}
         | 
| 87 | 
            +
                      else
         | 
| 88 | 
            +
                        object
         | 
| 89 | 
            +
                      end
         | 
| 90 | 
            +
                    end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                    def resolve(arg)
         | 
| 93 | 
            +
                      if arg.is_a?(String)
         | 
| 94 | 
            +
                        if arg.match(/^<<\s*(.*?)\s*>>$/)
         | 
| 95 | 
            +
                          self[$1]
         | 
| 96 | 
            +
                        else
         | 
| 97 | 
            +
                          arg.gsub(/\{\{(\{?)\s*(.*?)\s*\}\}\}?/) do
         | 
| 98 | 
            +
                            value = self[$2]
         | 
| 99 | 
            +
                            ($1 == "{") ? value.inspect : value
         | 
| 100 | 
            +
                          end
         | 
| 101 | 
            +
                        end
         | 
| 102 | 
            +
                      else
         | 
| 103 | 
            +
                        arg
         | 
| 104 | 
            +
                      end
         | 
| 105 | 
            +
                    end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                    def execute(command, user = nil)
         | 
| 108 | 
            +
                      if user
         | 
| 109 | 
            +
                        command = "sudo -u #{user} -H sh -c #{command.inspect}"
         | 
| 110 | 
            +
                      end
         | 
| 111 | 
            +
                      puts "🏄♀️ ~> ".gray + command.yellow
         | 
| 112 | 
            +
                      unless dry_run?
         | 
| 113 | 
            +
                        start = Time.now
         | 
| 114 | 
            +
                        result = []
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                        channel = ssh.open_channel do |ch|
         | 
| 117 | 
            +
                          ch.exec(command) do |ch|
         | 
| 118 | 
            +
                            ch.on_data do |_, data|
         | 
| 119 | 
            +
                              unless data.blank?
         | 
| 120 | 
            +
                                data = data.split("\n").reject(&:blank?)
         | 
| 121 | 
            +
                                puts "   " + data.join("\n   ")
         | 
| 122 | 
            +
                                result.concat data
         | 
| 123 | 
            +
                              end
         | 
| 124 | 
            +
                            end
         | 
| 125 | 
            +
                          end
         | 
| 126 | 
            +
                        end
         | 
| 127 | 
            +
                        channel.wait
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                        puts "   #{(Time.now - start).round(3)}s".gray
         | 
| 130 | 
            +
                        result.join("\n")
         | 
| 131 | 
            +
                      end
         | 
| 132 | 
            +
                    end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                    def home_directory
         | 
| 135 | 
            +
                      @home_directory ||= ssh.exec!("pwd").strip
         | 
| 136 | 
            +
                    end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                    def server_version
         | 
| 139 | 
            +
                      @server_version ||= begin
         | 
| 140 | 
            +
                        release = ssh.exec!("cat /etc/lsb-release")
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                        distribution = release.match(/DISTRIB_ID=(.*)/)[1].underscore
         | 
| 143 | 
            +
                        release = release.match(/DISTRIB_RELEASE=(.*)/)[1]
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                        [distribution, release].join("-")
         | 
| 146 | 
            +
                      end
         | 
| 147 | 
            +
                    end
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                  private
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                    def stack
         | 
| 152 | 
            +
                      @stack
         | 
| 153 | 
            +
                    end
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                    def ssh
         | 
| 156 | 
            +
                      @ssh ||= begin
         | 
| 157 | 
            +
                        if dry_run?
         | 
| 158 | 
            +
                          SSHMock.new
         | 
| 159 | 
            +
                        else
         | 
| 160 | 
            +
                          Net::SSH.start(fetch(:host), fetch(:user))
         | 
| 161 | 
            +
                        end
         | 
| 162 | 
            +
                      end
         | 
| 163 | 
            +
                    rescue StandardError => e
         | 
| 164 | 
            +
                      raise AuthenticationFailedError, e.message
         | 
| 165 | 
            +
                    end
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                  end
         | 
| 168 | 
            +
                end
         | 
| 169 | 
            +
              end
         | 
| 170 | 
            +
            end
         | 
| @@ -0,0 +1,262 @@ | |
| 1 | 
            +
            module VPS
         | 
| 2 | 
            +
              class CLI < Thor
         | 
| 3 | 
            +
                class Playbook
         | 
| 4 | 
            +
                  class Tasks
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                    class InvalidTaskError < VPS::CLI::Error; end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                    def self.available
         | 
| 9 | 
            +
                      public_instance_methods(false) - [:run]
         | 
| 10 | 
            +
                    end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                    def initialize(tasks)
         | 
| 13 | 
            +
                      @tasks = [tasks].flatten.compact
         | 
| 14 | 
            +
                    end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                    def run(state)
         | 
| 17 | 
            +
                      @tasks.collect do |task|
         | 
| 18 | 
            +
                        case task
         | 
| 19 | 
            +
                        when :continue # next
         | 
| 20 | 
            +
                        when :abort
         | 
| 21 | 
            +
                          raise Interrupt
         | 
| 22 | 
            +
                        else
         | 
| 23 | 
            +
                          run_task(state, task)
         | 
| 24 | 
            +
                        end
         | 
| 25 | 
            +
                      end
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    def run_tasks(state, options)
         | 
| 29 | 
            +
                      tasks = (state.resolve(options[:tasks]) || []).compact
         | 
| 30 | 
            +
                      Tasks.new(tasks).run(state)
         | 
| 31 | 
            +
                    end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                    def ensure(state, options)
         | 
| 34 | 
            +
                      argument = state.resolve(options[:argument])
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                      if state[argument].blank?
         | 
| 37 | 
            +
                        options[:fallbacks].each do |task|
         | 
| 38 | 
            +
                          unless (value = run_task(state, task.merge(as: argument))).blank?
         | 
| 39 | 
            +
                            set(state, argument, value)
         | 
| 40 | 
            +
                            break
         | 
| 41 | 
            +
                          end
         | 
| 42 | 
            +
                        end
         | 
| 43 | 
            +
                      end
         | 
| 44 | 
            +
                    end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                    def obtain_config(state, options)
         | 
| 47 | 
            +
                      VPS.read_config(state[:host]).tap do |config|
         | 
| 48 | 
            +
                        config.each do |key, value|
         | 
| 49 | 
            +
                          set(state, key, value)
         | 
| 50 | 
            +
                        end
         | 
| 51 | 
            +
                      end
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    def read_config(state, options)
         | 
| 55 | 
            +
                      VPS.read_config(state[:host], state.resolve(options[:key]))
         | 
| 56 | 
            +
                    end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                    def write_config(state, options)
         | 
| 59 | 
            +
                      config = {}
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                      options[:config].each do |key, spec|
         | 
| 62 | 
            +
                        spec = spec.with_indifferent_access if spec.is_a?(Hash)
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                        if spec.is_a?(Hash) && spec[:task]
         | 
| 65 | 
            +
                          config[key] = run_task(state, spec)
         | 
| 66 | 
            +
                        else
         | 
| 67 | 
            +
                          config[key] = state.resolve(spec)
         | 
| 68 | 
            +
                        end
         | 
| 69 | 
            +
                      end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                      unless state.dry_run?
         | 
| 72 | 
            +
                        VPS.write_config(state[:host], config)
         | 
| 73 | 
            +
                      end
         | 
| 74 | 
            +
                    end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                    def loop(state, options)
         | 
| 77 | 
            +
                      if (collection = (state.resolve(options[:through]) || []).compact).any?
         | 
| 78 | 
            +
                        puts_description(state, options)
         | 
| 79 | 
            +
                        as = state.resolve(options[:as])
         | 
| 80 | 
            +
                        collection.each do |item|
         | 
| 81 | 
            +
                          state.scope({as => item}) do
         | 
| 82 | 
            +
                            run_tasks(state, {:tasks => options[:run]})
         | 
| 83 | 
            +
                          end
         | 
| 84 | 
            +
                        end
         | 
| 85 | 
            +
                      end
         | 
| 86 | 
            +
                    end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                    def when(state, options)
         | 
| 89 | 
            +
                      if state[options[:boolean]]
         | 
| 90 | 
            +
                        puts_description(state, options)
         | 
| 91 | 
            +
                        run_tasks(state, {:tasks => options[:run]})
         | 
| 92 | 
            +
                      end
         | 
| 93 | 
            +
                    end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                    def confirm(state, options)
         | 
| 96 | 
            +
                      answer = Ask.confirm(question(options)) ? "y" : "n"
         | 
| 97 | 
            +
                      tasks = options[answer]
         | 
| 98 | 
            +
                      set(state, options, answer)
         | 
| 99 | 
            +
                      run_tasks(state, {:tasks => tasks})
         | 
| 100 | 
            +
                    end
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                    def select(state, options)
         | 
| 103 | 
            +
                      list = state.resolve(options[:options])
         | 
| 104 | 
            +
                      index = Ask.list(question(options), list)
         | 
| 105 | 
            +
                      set(state, options, list[index])
         | 
| 106 | 
            +
                    end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                    def multiselect(state, options)
         | 
| 109 | 
            +
                      names, labels, defaults = [], [], []
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                      options[:options].inject([names, labels, defaults]) do |_, (name, label)|
         | 
| 112 | 
            +
                        default = true
         | 
| 113 | 
            +
                        label = label.gsub(/ \[false\]$/) do
         | 
| 114 | 
            +
                          default = false
         | 
| 115 | 
            +
                          ""
         | 
| 116 | 
            +
                        end
         | 
| 117 | 
            +
                        names.push(name)
         | 
| 118 | 
            +
                        labels.push(label)
         | 
| 119 | 
            +
                        defaults.push(default)
         | 
| 120 | 
            +
                      end
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                      selected = Ask.checkbox(question(options), labels, default: defaults)
         | 
| 123 | 
            +
                      selected.each_with_index do |value, index|
         | 
| 124 | 
            +
                        name = names[index]
         | 
| 125 | 
            +
                        state[name] = value
         | 
| 126 | 
            +
                      end
         | 
| 127 | 
            +
                    end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                    def input(state, options)
         | 
| 130 | 
            +
                      answer = Ask.input(question(options), default: state.resolve(options[:default]))
         | 
| 131 | 
            +
                      set(state, options, answer)
         | 
| 132 | 
            +
                    end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                    def generate_file(state, options)
         | 
| 135 | 
            +
                      erb = VPS.read_template(state.resolve(options[:template]))
         | 
| 136 | 
            +
                      template = Erubis::Eruby.new(erb)
         | 
| 137 | 
            +
                      content = template.result(state.to_binding)
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                      unless state.dry_run?
         | 
| 140 | 
            +
                        if target = state.resolve(options[:target])
         | 
| 141 | 
            +
                          target = File.expand_path(target)
         | 
| 142 | 
            +
                          FileUtils.mkdir_p(File.dirname(target))
         | 
| 143 | 
            +
                          File.open(target, "w") do |file|
         | 
| 144 | 
            +
                            file.write(content)
         | 
| 145 | 
            +
                          end
         | 
| 146 | 
            +
                        end
         | 
| 147 | 
            +
                      end
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                      content
         | 
| 150 | 
            +
                    end
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                    def execute(state, options)
         | 
| 153 | 
            +
                      output = [options[:command]].flatten.inject(nil) do |_, command|
         | 
| 154 | 
            +
                        command = state.resolve(command)
         | 
| 155 | 
            +
                        puts "☕ ~> ".gray + command.yellow
         | 
| 156 | 
            +
                        unless state.dry_run?
         | 
| 157 | 
            +
                          start = Time.now
         | 
| 158 | 
            +
                          result = []
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                          IO.popen(command).each do |data|
         | 
| 161 | 
            +
                            unless data.blank?
         | 
| 162 | 
            +
                              data = data.split("\n").reject(&:blank?)
         | 
| 163 | 
            +
                              puts "   " + data.join("\n   ")
         | 
| 164 | 
            +
                              result.concat data
         | 
| 165 | 
            +
                            end
         | 
| 166 | 
            +
                          end
         | 
| 167 | 
            +
             | 
| 168 | 
            +
                          puts "   #{(Time.now - start).round(3)}s".gray
         | 
| 169 | 
            +
                          result.join("\n")
         | 
| 170 | 
            +
                        end
         | 
| 171 | 
            +
                      end
         | 
| 172 | 
            +
                      set(state, options, output)
         | 
| 173 | 
            +
                    end
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                    def remote_execute(state, options)
         | 
| 176 | 
            +
                      user = state.resolve(options[:user])
         | 
| 177 | 
            +
                      output = [options[:command]].flatten.inject(nil) do |_, command|
         | 
| 178 | 
            +
                        command = state.resolve(command)
         | 
| 179 | 
            +
                        state.execute(command, user)
         | 
| 180 | 
            +
                      end
         | 
| 181 | 
            +
                      set(state, options, output)
         | 
| 182 | 
            +
                    end
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                    def upload(state, options)
         | 
| 185 | 
            +
                      host = state[:host]
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                      file = state.resolve(options[:file])
         | 
| 188 | 
            +
                      remote_path = options[:remote_path] ? state.resolve(options[:remote_path]) : file
         | 
| 189 | 
            +
                      file = "-r #{file}" if File.directory?(file)
         | 
| 190 | 
            +
             | 
| 191 | 
            +
                      return if file.blank?
         | 
| 192 | 
            +
             | 
| 193 | 
            +
                      remote_path = remote_path.gsub("~", state.home_directory)
         | 
| 194 | 
            +
             | 
| 195 | 
            +
                      remote_execute(state, {:command => "mkdir -p #{File.dirname(remote_path)}"})
         | 
| 196 | 
            +
                      execute(state, {:command => "scp #{file} #{host}:#{remote_path} > /dev/tty"})
         | 
| 197 | 
            +
                    end
         | 
| 198 | 
            +
             | 
| 199 | 
            +
                    def sync(state, options)
         | 
| 200 | 
            +
                      host = state[:host]
         | 
| 201 | 
            +
             | 
| 202 | 
            +
                      directory = state.resolve(options[:directory])
         | 
| 203 | 
            +
                      remote_path = options[:remote_path] ? state.resolve(options[:remote_path]) : directory
         | 
| 204 | 
            +
             | 
| 205 | 
            +
                      return if directory.blank?
         | 
| 206 | 
            +
             | 
| 207 | 
            +
                      remote_path = remote_path.gsub("~", state.home_directory)
         | 
| 208 | 
            +
             | 
| 209 | 
            +
                      remote_execute(state, {:command => "mkdir -p #{File.dirname(remote_path)}"})
         | 
| 210 | 
            +
                      execute(state, {:command => "rsync #{options[:options]} #{directory} #{host}:#{remote_path} > /dev/tty"})
         | 
| 211 | 
            +
                    end
         | 
| 212 | 
            +
             | 
| 213 | 
            +
                    def playbook(state, options)
         | 
| 214 | 
            +
                      Playbook.run(state.resolve(options[:playbook]), state)
         | 
| 215 | 
            +
                    end
         | 
| 216 | 
            +
             | 
| 217 | 
            +
                    def print(state, options)
         | 
| 218 | 
            +
                      puts state.resolve(options[:message])
         | 
| 219 | 
            +
                    end
         | 
| 220 | 
            +
             | 
| 221 | 
            +
                  private
         | 
| 222 | 
            +
             | 
| 223 | 
            +
                    def run_task(state, task)
         | 
| 224 | 
            +
                      name, options = derive_task(task)
         | 
| 225 | 
            +
                      if name
         | 
| 226 | 
            +
                        puts_description(state, options) unless [:when, :loop].include?(name)
         | 
| 227 | 
            +
                        send(name, state, options)
         | 
| 228 | 
            +
                      else
         | 
| 229 | 
            +
                        raise InvalidTaskError, "Invalid task #{task.inspect}"
         | 
| 230 | 
            +
                      end
         | 
| 231 | 
            +
                    end
         | 
| 232 | 
            +
             | 
| 233 | 
            +
                    def derive_task(task)
         | 
| 234 | 
            +
                      if task.is_a?(Hash)
         | 
| 235 | 
            +
                        task = task.with_indifferent_access
         | 
| 236 | 
            +
                        name = task.delete(:task).to_sym
         | 
| 237 | 
            +
                        if Tasks.available.include?(name)
         | 
| 238 | 
            +
                          [name, task]
         | 
| 239 | 
            +
                        end
         | 
| 240 | 
            +
                      end
         | 
| 241 | 
            +
                    end
         | 
| 242 | 
            +
             | 
| 243 | 
            +
                    def puts_description(state, options)
         | 
| 244 | 
            +
                      if description = state.resolve(options[:description])
         | 
| 245 | 
            +
                        puts "\n== ".yellow + description.green
         | 
| 246 | 
            +
                      end
         | 
| 247 | 
            +
                    end
         | 
| 248 | 
            +
             | 
| 249 | 
            +
                    def question(options)
         | 
| 250 | 
            +
                      (options[:indent] == false ? "" : "   ") + options[:question]
         | 
| 251 | 
            +
                    end
         | 
| 252 | 
            +
             | 
| 253 | 
            +
                    def set(state, as, value)
         | 
| 254 | 
            +
                      if key = (as.is_a?(Hash) ? as[:as] : as)
         | 
| 255 | 
            +
                        state[key] = value
         | 
| 256 | 
            +
                      end
         | 
| 257 | 
            +
                    end
         | 
| 258 | 
            +
             | 
| 259 | 
            +
                  end
         | 
| 260 | 
            +
                end
         | 
| 261 | 
            +
              end
         | 
| 262 | 
            +
            end
         |