really 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3faf3d870217edbb133f4668240d7cb983161efd
4
+ data.tar.gz: a0ae4da9a06b9ac6830de179c1eebbccd90e65b3
5
+ SHA512:
6
+ metadata.gz: d7296fa8668ee667c0dc25c0039e7bf1469f4c11e5e9928eef0ec86b6d36abfe34b5714901940e56ed9453723cfa4812f012ea1a82f238c0ab8df8ce33002fff
7
+ data.tar.gz: b3921946d18ca45b4c0ffe26cd1a722da4b16655e63e8f9cbb2eb7baad99fe2b6d1fa1bbcbd4a6251e244b4726517b7023e38d1f2c5c2065c59456f581439d0a
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in really.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Cody Krieger
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,27 @@
1
+ # Really
2
+
3
+ A really (ha, a pun!) simple machine provisioning tool (like Chef or Puppet,
4
+ but way simpler). Heavily inspired by [Sprinkle](https://github.com/sprinkle-tool/sprinkle).
5
+
6
+ ## Really is a work in progress...
7
+
8
+ ...and as such, there is little documentation, except for the code itself, and
9
+ the example really.rb manifest in the root of the repository.
10
+
11
+ ## Installation
12
+
13
+ Or install it yourself as:
14
+
15
+ $ gem install really
16
+
17
+ ## Usage
18
+
19
+ TODO: Write usage instructions here
20
+
21
+ ## Contributing
22
+
23
+ 1. Fork it
24
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
25
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
26
+ 4. Push to the branch (`git push origin my-new-feature`)
27
+ 5. Create new Pull Request
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ end
7
+
8
+ task :default => :test
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'really'
4
+
5
+ Really.start_cli ARGV
@@ -0,0 +1,26 @@
1
+ require 'really/version'
2
+
3
+ require 'really/cli'
4
+ require 'really/logger'
5
+ require 'really/script'
6
+
7
+ module Really
8
+ class << self
9
+ def start_cli(args)
10
+ CLI.start args
11
+ end
12
+
13
+ def provision(options = {})
14
+ options = options.dup
15
+ message = "Invoked with options: #{options}"
16
+
17
+ logger.verbose = options.delete :verbose
18
+ logger.quiet = options.delete :quiet
19
+
20
+ logger.debug message
21
+
22
+ script = Script.new options.delete(:config_file), options
23
+ script.execute
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,22 @@
1
+ require 'thor'
2
+
3
+ module Really
4
+ class CLI < Thor
5
+ class_option :verbose, type: :boolean, aliases: '-v', default: false
6
+ class_option :quiet, type: :boolean, aliases: '-q', default: false
7
+
8
+ desc '--version', 'Prints the version.'
9
+ def version
10
+ puts "really v#{Really::VERSION}"
11
+ end
12
+
13
+ desc 'provision', 'Provision the hosts provided in really.rb or given on the command line.'
14
+ option :config_file, aliases: '-c', default: 'really.rb'
15
+ option :force, type: :boolean, aliases: '-f', default: false
16
+ option :test_run, type: :boolean, aliases: '-t', default: false
17
+ # option :runlist
18
+ def provision
19
+ Really.provision options
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,28 @@
1
+ module Really
2
+ class Command
3
+ attr_accessor :options
4
+
5
+ def initialize(command, options = {})
6
+ @command = command
7
+ @options = options
8
+ end
9
+
10
+ def command
11
+ "#{sudo_command}#{@command}"
12
+ end
13
+
14
+ def to_s
15
+ "`#{@command}`"
16
+ end
17
+
18
+ private
19
+
20
+ def sudo_command
21
+ "sudo -p '#{sudo_prompt}' " if @options[:sudo]
22
+ end
23
+
24
+ def sudo_prompt
25
+ 'sudo password: '
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,15 @@
1
+ module Really
2
+ module Commands
3
+ class << self
4
+ def register(*modules)
5
+ logger.debug "Registering modules: #{modules}"
6
+ modules.each { |mod| ::Really::Task.send :prepend, mod }
7
+ end
8
+ end
9
+ end
10
+ end
11
+
12
+ Dir[File.join(__dir__, 'commands', '*.rb')].each do |file|
13
+ filename = File.basename(file, File.extname(file))
14
+ require "really/commands/#{filename}"
15
+ end
@@ -0,0 +1,11 @@
1
+ module Really
2
+ module Commands
3
+ module BasicCommands
4
+ def command(command, options = {})
5
+ _add_command command, options
6
+ end
7
+ end
8
+ end
9
+ end
10
+
11
+ Really::Commands.register Really::Commands::BasicCommands
@@ -0,0 +1,11 @@
1
+ module Really
2
+ module Commands
3
+ module FileTransferCommands
4
+ def file(source_path, destination_path, options = {})
5
+ _add_file_transfer source_path, destination_path, options
6
+ end
7
+ end
8
+ end
9
+ end
10
+
11
+ Really::Commands.register Really::Commands::FileTransferCommands
@@ -0,0 +1,37 @@
1
+ module Really
2
+ module Commands
3
+ module PackageManagement
4
+ NONINTERACTIVE_ENV = "env DEBCONF_TERSE='yes' DEBIAN_PRIORITY='critical' DEBIAN_FRONTEND=noninteractive "
5
+
6
+ def install_package(package_name, options = {})
7
+ default_options = { sudo: true }
8
+ options = default_options.merge options
9
+ # FIXME: make platform-independent
10
+ command "#{NONINTERACTIVE_ENV}apt-get install -y #{package_name}", options
11
+ end
12
+
13
+ def install_package_dependencies(package_name, options = {})
14
+ default_options = { sudo: true }
15
+ options = default_options.merge options
16
+ # FIXME: make platform-independent
17
+ command "#{NONINTERACTIVE_ENV}apt-get build-dep -y #{package_name}", options
18
+ end
19
+
20
+ def update_available_packages(options = {})
21
+ default_options = { sudo: true }
22
+ options = default_options.merge options
23
+ # FIXME: make platform-independent
24
+ command "#{NONINTERACTIVE_ENV}apt-get update -y", options
25
+ end
26
+
27
+ def upgrade_installed_packages(options = {})
28
+ default_options = { sudo: true }
29
+ options = default_options.merge options
30
+ # FIXME: make platform-independent
31
+ command "#{NONINTERACTIVE_ENV}apt-get upgrade -y", options
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ Really::Commands.register Really::Commands::PackageManagement
@@ -0,0 +1,13 @@
1
+ require 'really/helpers/rendering_helper'
2
+
3
+ module Really
4
+ module Commands
5
+ module RenderingCommands
6
+ def render_template(*args)
7
+ Really::Helpers::RenderingHelper.render_template *args
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ Really::Commands.register Really::Commands::RenderingCommands
@@ -0,0 +1,17 @@
1
+ module Really
2
+ module Commands
3
+ module TextFileCommands
4
+ def append_text_to_file(text, file, options = {})
5
+ # FIXME: implement
6
+ command "echo 'we should append some text to '#{file}' here!'", options
7
+ end
8
+
9
+ def prepend_text_to_file(text, file, options = {})
10
+ # FIXME: implement
11
+ command "echo 'we should prepend some text to '#{file}' here!'", options
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ Really::Commands.register Really::Commands::TextFileCommands
@@ -0,0 +1,30 @@
1
+ require 'tsort'
2
+
3
+ module Really
4
+ class DependencyGraph
5
+ include TSort
6
+
7
+ def initialize(node_pool = {})
8
+ @node_pool = node_pool
9
+ @nodes = []
10
+ end
11
+
12
+ def add(node)
13
+ @nodes << node
14
+ end
15
+
16
+ alias_method :ordered_nodes, :tsort
17
+
18
+ def tsort_each_node(&block)
19
+ @nodes.each &block
20
+ end
21
+
22
+ def tsort_each_child(node, &block)
23
+ @node_pool[node.name].dependencies.each do |dependency_name|
24
+ node = @node_pool[dependency_name]
25
+ raise "Dependency '#{dependency_name}' does not exist." if node.nil?
26
+ block.call node
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,4 @@
1
+ Dir[File.join(__dir__, 'drivers', '*.rb')].each do |file|
2
+ filename = File.basename(file, File.extname(file))
3
+ require "really/drivers/#{filename}"
4
+ end
@@ -0,0 +1,64 @@
1
+ require 'really/command'
2
+ require 'really/file_transfer_command'
3
+
4
+ module Really
5
+ module Drivers
6
+ class DriverBase
7
+ attr_accessor :role_names
8
+
9
+ def initialize(&block)
10
+ @role_names = []
11
+ instance_eval &block if block
12
+ end
13
+
14
+ def to_s
15
+ "<#{self.class} roles:#{@role_names}>"
16
+ end
17
+
18
+ # Public API for use within `via` blocks in really.rb scripts
19
+
20
+ def roles(*roles)
21
+ @role_names += roles
22
+ end
23
+ alias_method :role, :roles
24
+
25
+ # Internal public API
26
+
27
+ def execute_task(task, options = {})
28
+ task.commands.each do |command|
29
+ logger.debug "command:#{command}"
30
+
31
+ if command.kind_of?(FileTransferCommand)
32
+ transfer_file command.source_path, command.destination_path, command.options
33
+ else
34
+ execute_command command, command.options unless options[:test_run]
35
+ end
36
+ end
37
+ end
38
+
39
+ def open
40
+ # Subclasses can override this to perform any driver initialization
41
+ # that is not appropriate for its constructor (like connecting to
42
+ # remote hosts, etc.).
43
+ end
44
+
45
+ def close
46
+ # Subclasses can override this to perform any necessary driver teardown.
47
+ end
48
+
49
+ protected
50
+
51
+ def execute_command(command, options = {})
52
+ raise "Subclasses must implement #execute_command."
53
+ end
54
+
55
+ def transfer_file(source_path, destination_path, options = {})
56
+ raise "Subclasses must implement #transfer_file."
57
+ end
58
+
59
+ def command_exited_with_exit_code(exit_code, options = {})
60
+ raise "Command failed with exit code #{exit_code}." unless exit_code == 0 || options[:allow_failure]
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,36 @@
1
+ require 'open3'
2
+
3
+ module Really
4
+ module Drivers
5
+ class Local < DriverBase
6
+ def initialize(*args)
7
+ # Super does an instance_eval on the given block, so we need to call
8
+ # super *after* setting up our internal state.
9
+ super *args
10
+ end
11
+
12
+ protected
13
+
14
+ def execute_command(command, options = {})
15
+ stdin, stdout, stderr, thread = Open3.popen3(command.command)
16
+ exit_status = thread.value.exitstatus
17
+
18
+ stdout_string = stdout.read
19
+ stderr_string = stderr.read
20
+
21
+ if options[:log_output]
22
+ # FIXME: centralize this in DriverBase?
23
+ logger.status "Output of #{command}:", header: arrow
24
+ stdout.each_line { |line| logger.status line, header: :arrow }
25
+ stderr.each_line { |line| logger.status line, header: :arrow }
26
+ end
27
+
28
+ command_exited_with_exit_code exit_status, options
29
+ end
30
+
31
+ def transfer_file(source_path, destination_path, options = {})
32
+ # FIXME: transfer file
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,145 @@
1
+ require 'net/ssh'
2
+ require 'net/ssh/gateway'
3
+ require 'net/scp'
4
+
5
+ require 'strscan'
6
+
7
+ module Really
8
+ module Drivers
9
+ class SSH < DriverBase
10
+ KILOBYTE = 1024
11
+ SCP_CHUNK_SIZE = 32 * KILOBYTE
12
+
13
+ def initialize(*args)
14
+ @user = "root"
15
+ @hosts = []
16
+ @port = 22
17
+ @keys = '~/.ssh/id_rsa'
18
+ @connections = {}
19
+
20
+ # Super does an instance_eval on the given block, so we need to call
21
+ # super *after* setting up our internal state.
22
+ super *args
23
+ end
24
+
25
+ # Public API for use in `driver` blocks in really.rb scripts
26
+
27
+ def user(user)
28
+ @user = user
29
+ end
30
+
31
+ def hosts(*hosts)
32
+ @hosts += hosts
33
+ end
34
+ alias_method :host, :hosts
35
+
36
+ def port(port)
37
+ @port = port.to_i
38
+ end
39
+
40
+ def password(password)
41
+ @password = password
42
+ end
43
+
44
+ def keys(keys)
45
+ @keys = File.expand_path keys
46
+ end
47
+
48
+ def gateway(gateway)
49
+ @gateway = gateway
50
+ end
51
+
52
+ def open
53
+ @hosts.each do |host|
54
+ key = connection_key @user, host, @port
55
+ options = { password: @password, keys: @keys, port: @port }
56
+ options.delete :password unless @password
57
+ options.delete :keys unless @keys
58
+
59
+ if @gateway
60
+ @connection_gateway ||= Net::SSH::Gateway.new @gateway, @user
61
+ @connections[key] ||= gateway.ssh host, @user, options
62
+ else
63
+ @connections[key] ||= Net::SSH.start host, @user, options
64
+ end
65
+ end
66
+ end
67
+
68
+ def close
69
+ @connections.values.each { |connection| connection.close }
70
+ @connection_gateway.shutdown! if @connection_gateway
71
+ end
72
+
73
+ protected
74
+
75
+ # Required overrides from Really::Drivers::DriverBase
76
+
77
+ def execute_command(command, options = {})
78
+ @connections.values.each do |connection|
79
+ stdout_data = StringScanner.new ""
80
+ stderr_data = StringScanner.new ""
81
+
82
+ connection.open_channel do |channel|
83
+ channel.on_data do |channel, data|
84
+ stdout_data << data
85
+ while (line = stdout_data.scan_until(/\n/))
86
+ if options[:log_output]
87
+ logger.status line, header: :arrow, header_color: :yellow
88
+ else
89
+ logger.debug line, header: :arrow, header_color: :yellow
90
+ end
91
+ end
92
+ end
93
+
94
+ channel.on_extended_data do |channel, type, data|
95
+ stderr_data << data
96
+ while (line = stderr_data.scan_until(/\n/))
97
+ if options[:log_output]
98
+ logger.status line, header: :arrow, header_color: :red
99
+ else
100
+ logger.debug line, header: :arrow, header_color: :red
101
+ end
102
+ end
103
+ end
104
+
105
+ channel.on_request("exit-status") do |channel, data|
106
+ command_exited_with_exit_code data.read_long, options
107
+ end
108
+
109
+ channel.on_request("exit-signal") do |channel, data|
110
+ logger.error "Command #{command} signaled: #{data.read_long}"
111
+ end
112
+
113
+ channel.exec(command.command) do |channel, success|
114
+ raise "Unable to execute remote command #{command}." unless success
115
+ end
116
+ end
117
+
118
+ logger.status stdout_data.rest, header: :arrow unless stdout_data.eos?
119
+ logger.error stderr_data.rest, header: :arrow unless stderr_data.eos?
120
+
121
+ connection.loop
122
+ end
123
+ end
124
+
125
+ def transfer_file(source_path, destination_path, options = {})
126
+ @connections.values.each do |connection|
127
+ begin
128
+ logger.debug "Transferring '#{source_path}' to '#{destination_path}' via SCP..."
129
+ scp = Net::SCP.new connection
130
+ scp.upload! source_path, destination_path, recursive: options[:recursive], chunk_size: SCP_CHUNK_SIZE
131
+ rescue RuntimeError => error
132
+ raise "File transfer failed: '#{source_path}' => '#{destination_path}' (permission denied)." if error.message =~ /Permission denied/
133
+ raise error
134
+ end
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ def connection_key(user, host, port)
141
+ "#{user}@#{host}:#{port}"
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,42 @@
1
+ require 'really/command'
2
+ require 'really/helpers/rendering_helper'
3
+ require 'tempfile'
4
+
5
+ module Really
6
+ class FileTransferCommand < Command
7
+ attr_accessor :destination_path
8
+
9
+ def initialize(source_path, destination_path, options = {})
10
+ super nil, options
11
+ @source_path = source_path
12
+ @destination_path = destination_path
13
+ end
14
+
15
+ def source_path
16
+ return rendered_source_path if @options[:render]
17
+ @source_path
18
+ end
19
+
20
+ def to_s
21
+ "<#{self.class} source:'#{@source_path}' dest:'#{@destination_path}'>"
22
+ end
23
+
24
+ private
25
+
26
+ def rendered_source_path
27
+ return @rendered_file.path if @rendered_file
28
+
29
+ @rendered_file = Tempfile.new File.basename(@source_path)
30
+
31
+ args = [@source_path]
32
+ args << @options[:context] if @options[:context]
33
+
34
+ @rendered_file.print Really::Helpers::RenderingHelper.render_template(*args)
35
+ @rendered_file.close
36
+
37
+ logger.debug "Rendered eRuby template to tempfile '#{@rendered_file.path}'."
38
+
39
+ @rendered_file.path
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,21 @@
1
+ require 'erubis'
2
+
3
+ module Really
4
+ module Helpers
5
+ module RenderingHelper
6
+ class << self
7
+ def render_template(path, context = binding)
8
+ path = File.expand_path path
9
+ raise "File at '#{path}' is not readable. (Does it exist?)" unless File.readable?(path)
10
+
11
+ logger.debug "Rendering eRuby template at path '#{path}'..."
12
+
13
+ eruby = Erubis::Eruby.new File.read(path)
14
+ result = eruby.result context
15
+
16
+ result
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,72 @@
1
+ module Really
2
+ class Logger
3
+ attr_accessor :verbose, :quiet
4
+
5
+ def initialize
6
+ $stdout.sync = true
7
+ @verbose = false
8
+ @quiet = false
9
+ end
10
+
11
+ def status(message, options = {})
12
+ log message, options
13
+ end
14
+
15
+ def error(message, options = {})
16
+ default_options = { header: 'error:', header_color: :red, override_quiet: true }
17
+ options = default_options.merge options
18
+ log message, options
19
+ end
20
+
21
+ def warning(message, options = {})
22
+ default_options = { header: 'warning:', header_color: :yellow }
23
+ options = default_options.merge options
24
+ log message, options
25
+ end
26
+
27
+ def debug(message, options = {})
28
+ default_options = { header: 'debug:', header_color: :pink }
29
+ options = default_options.merge options
30
+ log message, options if @verbose
31
+ end
32
+
33
+ private
34
+
35
+ def log(message, options = {})
36
+ default_options = { header: nil, header_color: :yellow, override_quiet: false }
37
+ options = default_options.merge options
38
+
39
+ options[:header] = ' -->' if options[:header] == :arrow
40
+ options[:header] = self.send options[:header_color], options[:header] if options[:header_color]
41
+
42
+ puts "#{"#{options[:header]} " if options[:header]}#{message}" unless @quiet && !options[:override_quiet]
43
+ end
44
+
45
+ def red(str)
46
+ color_str(str, 31)
47
+ end
48
+
49
+ def green(str)
50
+ color_str(str, 32)
51
+ end
52
+
53
+ def yellow(str)
54
+ color_str(str, 33)
55
+ end
56
+
57
+ def pink(str)
58
+ color_str(str, 35)
59
+ end
60
+
61
+ def color_str(str, color_code)
62
+ return nil if str.nil? || str.empty?
63
+ "\e[#{color_code}m#{str}\e[0m"
64
+ end
65
+ end
66
+ end
67
+
68
+ class Object
69
+ def logger
70
+ @@__really_logger__ ||= Really::Logger.new
71
+ end
72
+ end
@@ -0,0 +1,34 @@
1
+ module Really
2
+ class Role
3
+ attr_reader :name
4
+ attr_accessor :dependencies, :task_descriptors
5
+
6
+ def initialize(name, options = {}, &block)
7
+ @name = name
8
+ @options = options
9
+ @dependencies = []
10
+ @task_descriptors = []
11
+
12
+ dependencies = options[:depends_on] || []
13
+ @dependencies += [dependencies].flatten
14
+
15
+ instance_eval &block
16
+ end
17
+
18
+ def eql?(role)
19
+ @name == role.name
20
+ end
21
+
22
+ def hash
23
+ @name.hash
24
+ end
25
+
26
+ def to_s
27
+ "<#{self.class} #{@name}>"
28
+ end
29
+
30
+ def task(name, options = {})
31
+ @task_descriptors << { task_name: name, options: options }
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,103 @@
1
+ require 'really/drivers'
2
+ require 'really/role'
3
+ require 'really/task'
4
+
5
+ require 'really/dependency_graph'
6
+
7
+ module Really
8
+ class Script
9
+ def initialize(path, options = {})
10
+ path = File.expand_path path
11
+ raise "Script at '#{path}' is not readable. :( (Does it exist?)" unless File.readable?(path)
12
+
13
+ @options = options
14
+ @tasks = {}
15
+ @roles = {}
16
+ @drivers = []
17
+
18
+ instance_eval File.read(path)
19
+ end
20
+
21
+ # Public API for use in really.rb scripts
22
+
23
+ def task(name, options = {}, &block)
24
+ name = name.to_sym
25
+ raise "Duplicate definition of group name '#{name}'." if @tasks.has_key?(name)
26
+
27
+ task = Task.new(name, options, &block)
28
+ @tasks[name] = task
29
+ task
30
+ end
31
+
32
+ def role(name, options = {}, &block)
33
+ name = name.to_sym
34
+ raise "Duplicate definition of role name '#{name}'." if @roles.has_key?(name)
35
+
36
+ role = Role.new(name, options, &block)
37
+ @roles[name] = role
38
+ role
39
+ end
40
+
41
+ def configure(&block)
42
+ instance_eval &block
43
+ end
44
+
45
+ def via(name, &block)
46
+ name = name.to_s.capitalize
47
+ name = "SSH" if name == "Ssh"
48
+ klass = eval "Really::Drivers::#{name}"
49
+
50
+ driver = klass.new &block
51
+ @drivers << driver
52
+ driver
53
+ end
54
+
55
+ # Internal public API
56
+
57
+ def execute(role_names = [])
58
+ @drivers.each do |driver|
59
+ driver.open
60
+
61
+ role_graph = DependencyGraph.new @roles
62
+
63
+ # All drivers implicitly get the magic :default role.
64
+ role_graph.add @roles[:default] if @roles.has_key?(:default)
65
+
66
+ driver.role_names.each do |role_name|
67
+ raise "Role '#{role_name}' does not exist." unless @roles.has_key?(role_name)
68
+ role_graph.add @roles[role_name]
69
+ end
70
+
71
+ all_tasks = Hash[@tasks.collect { |key, value| [key, value.dup] }]
72
+ task_graph = DependencyGraph.new all_tasks
73
+
74
+ roles = role_graph.ordered_nodes
75
+
76
+ roles.each do |role|
77
+ role.task_descriptors.each do |descriptor|
78
+ task_name = descriptor[:task_name]
79
+ raise "Task '#{task_name}' does not exist." unless all_tasks.has_key?(task_name)
80
+
81
+ task = all_tasks[task_name]
82
+ task.options.merge! descriptor[:options]
83
+ task_graph.add task
84
+ end
85
+ end
86
+
87
+ logger.status "[#{roles.length > 1 ? 'roles' : 'role'}: #{roles.collect(&:name).join ', '}]"
88
+
89
+ task_graph.ordered_nodes.each do |task|
90
+ logger.status "task:#{task.name}", header: '***', header_color: :green
91
+ driver.execute_task task, @options
92
+ end
93
+
94
+ driver.close
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ # stuff
101
+
102
+ end
103
+ end
@@ -0,0 +1,60 @@
1
+ require 'really/command'
2
+ require 'really/file_transfer_command'
3
+
4
+ module Really
5
+ class Task
6
+ attr_reader :name
7
+ attr_accessor :options, :dependencies
8
+
9
+ def initialize(name, options = {}, &block)
10
+ @name = name
11
+ @options = options
12
+ @block = block
13
+ @dependencies = []
14
+ @commands = []
15
+
16
+ dependencies = options[:depends_on] || []
17
+ @dependencies += [dependencies].flatten
18
+ end
19
+
20
+ def eql?(task)
21
+ @name == task.name
22
+ end
23
+
24
+ def hash
25
+ @name.hash
26
+ end
27
+
28
+ def to_s
29
+ "<#{self.class} #{@name}>"
30
+ end
31
+
32
+ # Internal public API
33
+
34
+ def commands
35
+ instance_eval &@block if @block && @commands.empty?
36
+ @commands
37
+ end
38
+
39
+ protected
40
+
41
+ # Internal API for modules that add methods to Really::Task
42
+
43
+ def _add_command(command, options = {})
44
+ @commands << Command.new(command, options)
45
+ end
46
+
47
+ def _add_file_transfer(source_path, destination_path, options = {})
48
+ filename = File.basename destination_path
49
+ tmp_path = "/tmp/_really_#{filename}"
50
+
51
+ @commands << FileTransferCommand.new(source_path, tmp_path, options)
52
+
53
+ _add_command "mv '#{tmp_path}' '#{destination_path}'", options
54
+ _add_command "chown #{'-R ' if options[:recursive]}#{options[:user]}:#{options[:user]} #{destination_path}", options if options[:user]
55
+ _add_command "chmod #{'-R ' if options[:recursive]}#{options[:mode]} #{destination_path}", options if options[:mode]
56
+ end
57
+ end
58
+ end
59
+
60
+ require 'really/commands'
@@ -0,0 +1,3 @@
1
+ module Really
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'really/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "really"
8
+ spec.version = Really::VERSION
9
+ spec.authors = ["Cody Krieger"]
10
+ spec.email = ["cody@krieger.io"]
11
+ spec.description = %q{A really simple machine provisioning tool.}
12
+ spec.summary = %q{A really simple machine provisioning tool.}
13
+ spec.homepage = "https://github.com/codykrieger/really"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "minitest"
24
+
25
+ spec.add_dependency "erubis", "~> 2.7.0"
26
+ spec.add_dependency "net-scp", "~> 1.1.2"
27
+ spec.add_dependency "net-ssh", "~> 2.6.8"
28
+ spec.add_dependency "net-ssh-gateway", "~> 1.2.0"
29
+ spec.add_dependency "thor", "~> 0.18.1"
30
+ end
@@ -0,0 +1,76 @@
1
+ # tasks do not have verify blocks; this should help enforce the convention that commands are idempotent
2
+
3
+ task :install_vim do
4
+ install_package 'vim'
5
+ end
6
+
7
+ task :install_emacs do
8
+ install_package 'emacs' # ...ew
9
+ end
10
+
11
+ task :install_zsh do
12
+ install_package 'zsh'
13
+ end
14
+
15
+ task :install_git do
16
+ install_package 'git-core'
17
+ end
18
+
19
+ task :configure_git, depends_on: :install_git do
20
+ command "git config --global user.name 'Cody Krieger'"
21
+ command "git config --global user.email 'foo@example.com'"
22
+ end
23
+
24
+ task :install_editors, depends_on: [:install_vim, :install_emacs] # block optional! (this is a meta-task)
25
+
26
+ task :testing do
27
+ install_package 'vim'
28
+ install_package 'zsh'
29
+ install_package_dependencies 'ruby'
30
+ file '~/projects/sprinkle/app-server/iptables.up.rules.erb', '/etc/iptables.up.rules', sudo: true, render: true, context: { ssh_port: 2222 }
31
+ end
32
+
33
+ # the magic :default role runs tasks on *all* hosts (before other roles), no matter what (well, unless --no-default is specified)
34
+ role :default do
35
+ task :install_editors
36
+ end
37
+
38
+ role :git_server do
39
+ task :install_git
40
+ task :configure_git
41
+ task :testing
42
+ end
43
+
44
+ role :other_server, depends_on: [:git_server] do
45
+ task :install_zsh
46
+ end
47
+
48
+ role :testing do
49
+ task :testing
50
+ end
51
+
52
+ configure do
53
+ via :ssh do
54
+ host "127.0.0.1"
55
+ port 2222
56
+ user "vagrant"
57
+ # password "vagrant"
58
+ keys "~/.vagrant.d/insecure_private_key"
59
+
60
+ role :git_server
61
+ end
62
+
63
+ # via :ssh do
64
+ # hosts "example.com", "secretwebsite.com"
65
+ # user "deploy"
66
+ # keys "~/.ssh/id_rsa"
67
+ # gateway "gateway.example.com"
68
+
69
+ # roles :other_server, :some_other_role
70
+ # end
71
+
72
+ # via :local do
73
+ # role :other_server
74
+ # role :testing
75
+ # end
76
+ end
@@ -0,0 +1,4 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'really'
3
+
4
+ require 'minitest/autorun'
@@ -0,0 +1,11 @@
1
+ require 'minitest_helper'
2
+
3
+ class TestReally < MiniTest::Unit::TestCase
4
+ def test_that_it_has_a_version_number
5
+ refute_nil ::Really::VERSION
6
+ end
7
+
8
+ def test_it_does_something_useful
9
+ assert false
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,191 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: really
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Cody Krieger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-08-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: erubis
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 2.7.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: 2.7.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: net-scp
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: 1.1.2
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: 1.1.2
83
+ - !ruby/object:Gem::Dependency
84
+ name: net-ssh
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: 2.6.8
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: 2.6.8
97
+ - !ruby/object:Gem::Dependency
98
+ name: net-ssh-gateway
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ~>
102
+ - !ruby/object:Gem::Version
103
+ version: 1.2.0
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ~>
109
+ - !ruby/object:Gem::Version
110
+ version: 1.2.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: thor
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: 0.18.1
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ~>
123
+ - !ruby/object:Gem::Version
124
+ version: 0.18.1
125
+ description: A really simple machine provisioning tool.
126
+ email:
127
+ - cody@krieger.io
128
+ executables:
129
+ - really
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - .gitignore
134
+ - .travis.yml
135
+ - Gemfile
136
+ - LICENSE
137
+ - README.md
138
+ - Rakefile
139
+ - bin/really
140
+ - lib/really.rb
141
+ - lib/really/cli.rb
142
+ - lib/really/command.rb
143
+ - lib/really/commands.rb
144
+ - lib/really/commands/basic_commands.rb
145
+ - lib/really/commands/file_transfer_commands.rb
146
+ - lib/really/commands/package_management.rb
147
+ - lib/really/commands/rendering_commands.rb
148
+ - lib/really/commands/text_file_commands.rb
149
+ - lib/really/dependency_graph.rb
150
+ - lib/really/drivers.rb
151
+ - lib/really/drivers/driver_base.rb
152
+ - lib/really/drivers/local.rb
153
+ - lib/really/drivers/ssh.rb
154
+ - lib/really/file_transfer_command.rb
155
+ - lib/really/helpers/rendering_helper.rb
156
+ - lib/really/logger.rb
157
+ - lib/really/role.rb
158
+ - lib/really/script.rb
159
+ - lib/really/task.rb
160
+ - lib/really/version.rb
161
+ - really.gemspec
162
+ - really.rb
163
+ - test/minitest_helper.rb
164
+ - test/test_really.rb
165
+ homepage: https://github.com/codykrieger/really
166
+ licenses:
167
+ - MIT
168
+ metadata: {}
169
+ post_install_message:
170
+ rdoc_options: []
171
+ require_paths:
172
+ - lib
173
+ required_ruby_version: !ruby/object:Gem::Requirement
174
+ requirements:
175
+ - - '>='
176
+ - !ruby/object:Gem::Version
177
+ version: '0'
178
+ required_rubygems_version: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - '>='
181
+ - !ruby/object:Gem::Version
182
+ version: '0'
183
+ requirements: []
184
+ rubyforge_project:
185
+ rubygems_version: 2.0.3
186
+ signing_key:
187
+ specification_version: 4
188
+ summary: A really simple machine provisioning tool.
189
+ test_files:
190
+ - test/minitest_helper.rb
191
+ - test/test_really.rb