makitzo 0.1.2
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.
- data/Gemfile +12 -0
- data/Gemfile.lock +27 -0
- data/LICENSE.txt +20 -0
- data/README.mdown +22 -0
- data/RESOURCES +2 -0
- data/Rakefile +51 -0
- data/VERSION +1 -0
- data/bin/makitzo +6 -0
- data/lib/makitzo/application.rb +151 -0
- data/lib/makitzo/application_aware.rb +19 -0
- data/lib/makitzo/cli.rb +71 -0
- data/lib/makitzo/config.rb +148 -0
- data/lib/makitzo/file_system.rb +24 -0
- data/lib/makitzo/logging/blackhole.rb +16 -0
- data/lib/makitzo/logging/collector.rb +170 -0
- data/lib/makitzo/logging/colorize.rb +38 -0
- data/lib/makitzo/memoized_proc.rb +15 -0
- data/lib/makitzo/migrations/commands.rb +11 -0
- data/lib/makitzo/migrations/generator.rb +24 -0
- data/lib/makitzo/migrations/migration.rb +69 -0
- data/lib/makitzo/migrations/migrator.rb +87 -0
- data/lib/makitzo/migrations/paths.rb +7 -0
- data/lib/makitzo/monkeys/array.rb +10 -0
- data/lib/makitzo/monkeys/bangify.rb +15 -0
- data/lib/makitzo/monkeys/net-ssh.rb +73 -0
- data/lib/makitzo/monkeys/string.rb +15 -0
- data/lib/makitzo/multiplexed_reader.rb +26 -0
- data/lib/makitzo/settings.rb +30 -0
- data/lib/makitzo/ssh/commands/apple.rb +104 -0
- data/lib/makitzo/ssh/commands/file_system.rb +59 -0
- data/lib/makitzo/ssh/commands/file_transfer.rb +9 -0
- data/lib/makitzo/ssh/commands/http.rb +51 -0
- data/lib/makitzo/ssh/commands/makitzo.rb +46 -0
- data/lib/makitzo/ssh/commands/ruby.rb +18 -0
- data/lib/makitzo/ssh/commands/unix.rb +7 -0
- data/lib/makitzo/ssh/context.rb +91 -0
- data/lib/makitzo/ssh/multi.rb +79 -0
- data/lib/makitzo/store/mysql.rb +176 -0
- data/lib/makitzo/store/skeleton.rb +46 -0
- data/lib/makitzo/world/host.rb +84 -0
- data/lib/makitzo/world/named_entity.rb +41 -0
- data/lib/makitzo/world/query.rb +54 -0
- data/lib/makitzo/world/role.rb +4 -0
- data/lib/makitzo.rb +90 -0
- data/makitzo.gemspec +106 -0
- data/templates/migration.erb +9 -0
- data/test/helper.rb +17 -0
- data/test/test_makitzo.rb +7 -0
- metadata +222 -0
@@ -0,0 +1,104 @@
|
|
1
|
+
module Makitzo; module SSH; module Commands
|
2
|
+
module Apple
|
3
|
+
def mount_dmg(path)
|
4
|
+
mount_status = exec("hdiutil attach -puppetstrings #{x(path)}")
|
5
|
+
if mount_status.error?
|
6
|
+
logger.warn("unable to mount #{path}")
|
7
|
+
false
|
8
|
+
else
|
9
|
+
mount_status.stdout.split("\n").reverse.each do |line|
|
10
|
+
chunks = line.split(/\t+/)
|
11
|
+
if chunks.length == 3
|
12
|
+
mount_point = chunks[2].strip
|
13
|
+
unless mount_point.empty?
|
14
|
+
logger.success("#{path} mounted at #{mount_point}")
|
15
|
+
return mount_point
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def unmount_dmg(path)
|
24
|
+
unmount_status = exec("hdiutil detach #{x(path)}")
|
25
|
+
if unmount_status.error?
|
26
|
+
logger.warn("unable to unmount #{path}")
|
27
|
+
false
|
28
|
+
else
|
29
|
+
logger.success("#{path} unmounted")
|
30
|
+
true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def install_app(app, target = '/Applications', backup_file = nil)
|
35
|
+
target_dir = File.join(target, File.basename(app))
|
36
|
+
exec("test -d #{x(target_dir)} && rm -rf #{x(target_dir)}")
|
37
|
+
if exec("cp -R #{x(app)} #{x(target)}").success?
|
38
|
+
logger.success("app #{app} installed to #{target}")
|
39
|
+
true
|
40
|
+
else
|
41
|
+
logger.warn("failed to install #{app} to #{target}")
|
42
|
+
false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def install_pkg(pkg)
|
47
|
+
if exec("installer -pkg #{x(pkg)} -target /").success?
|
48
|
+
logger.success("package #{pkg} installed to /")
|
49
|
+
true
|
50
|
+
else
|
51
|
+
logger.warn("failed to install package #{pkg}")
|
52
|
+
false
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def shutdown_at(time)
|
57
|
+
sudo do
|
58
|
+
unless exec("pmset schedule shutdown \"#{time}\"").success?
|
59
|
+
logger.error("couldn't set poweroff time")
|
60
|
+
return false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
# format of restart_time is mm/dd/yy HH:MM:ss
|
67
|
+
def shutdown(restart_time = nil)
|
68
|
+
sudo do
|
69
|
+
if restart_time
|
70
|
+
res = exec("pmset schedule poweron \"#{restart_time}\"")
|
71
|
+
unless res.success?
|
72
|
+
logger.error("couldn't set restart time")
|
73
|
+
return false
|
74
|
+
end
|
75
|
+
end
|
76
|
+
res = exec("shutdown -h now")
|
77
|
+
res.success?
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def daily_shutdown
|
82
|
+
tomorrow = Time.now + 86400
|
83
|
+
shutdown(time.strftime("%m/%d/%y 08:45:00"))
|
84
|
+
end
|
85
|
+
|
86
|
+
def reboot
|
87
|
+
sudo { exec('reboot') }
|
88
|
+
end
|
89
|
+
|
90
|
+
def serial_number
|
91
|
+
res = exec("system_profiler SPHardwareDataType | grep 'Serial Number' | awk '{ print $4; }'")
|
92
|
+
res.success? ? res.stdout.strip : nil
|
93
|
+
end
|
94
|
+
|
95
|
+
def use_network_time_server(address)
|
96
|
+
sudo do
|
97
|
+
exec!("systemsetup -setnetworktimeserver \"#{x(address)}\"")
|
98
|
+
exec!("systemsetup -setusingnetworktime on")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
bangify :mount_dmg, :unmount_dmg, :install_app, 'Makitzo::SSH::CommandFailed'
|
103
|
+
end
|
104
|
+
end; end; end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Makitzo; module SSH; module Commands
|
2
|
+
module FileSystem
|
3
|
+
|
4
|
+
#
|
5
|
+
# Predicates... these never log extra info
|
6
|
+
|
7
|
+
def which?(executable)
|
8
|
+
exec("which #{executable}").success?
|
9
|
+
end
|
10
|
+
|
11
|
+
def dir_exists?(dir)
|
12
|
+
exec("test -d #{dir}").success?
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def cd(dir)
|
17
|
+
exec("cd #{x(dir)}")
|
18
|
+
end
|
19
|
+
|
20
|
+
def mv(source, destination)
|
21
|
+
exec("mv #{x(source)} #{x(destination)}").success?
|
22
|
+
end
|
23
|
+
|
24
|
+
# Ensure a directory exists.
|
25
|
+
# Log and raise CommandFailed otherwise
|
26
|
+
def require_dir!(dir, friendly_name = nil)
|
27
|
+
friendly_name ||= dir
|
28
|
+
unless dir_exists?(dir)
|
29
|
+
logger.error "#{friendly_name} (#{dir}) is not a directory"
|
30
|
+
raise CommandFailed
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Check that a directory exists and attempt to create it if missing
|
35
|
+
# Log and raise CommandFailed if can't create dir
|
36
|
+
def find_or_create_dir!(directory, friendly_name = nil)
|
37
|
+
friendly_name ||= directory
|
38
|
+
if !dir_exists?(directory)
|
39
|
+
mkdir = exec("mkdir -p #{directory}")
|
40
|
+
if mkdir.error?
|
41
|
+
logger.error "Failed to create #{friendly_name} (#{directory})"
|
42
|
+
raise CommandFailed
|
43
|
+
else
|
44
|
+
logger.success "#{friendly_name} directory created"
|
45
|
+
end
|
46
|
+
else
|
47
|
+
logger.success "#{friendly_name} directory located"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def rm_rf!(directory, friendly_name = nil)
|
52
|
+
friendly_name ||= directory
|
53
|
+
if exec("rm -rf #{directory}").error?
|
54
|
+
logger.error "could not delete #{friendly_name} (#{directory})"
|
55
|
+
raise CommandFailed
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end; end; end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Makitzo; module SSH; module Commands
|
2
|
+
module FileTransfer
|
3
|
+
def scp_upload(local_path, remote_path)
|
4
|
+
logger.info "scp: '#{local_path}' -> '#{remote_path}'"
|
5
|
+
scp = Net::SCP.new(connection)
|
6
|
+
scp.upload!(local_path, remote_path)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end; end; end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Makitzo; module SSH; module Commands
|
2
|
+
module HTTP
|
3
|
+
|
4
|
+
# downloads a url -> path, using either curl or wget
|
5
|
+
# logs a warning if neither is present
|
6
|
+
# TODO: hosts/roles should be able to specify their preferred d/l mechanism
|
7
|
+
def download(url, path)
|
8
|
+
if which?("curl")
|
9
|
+
download_with_curl(url, path)
|
10
|
+
elsif which?("wget")
|
11
|
+
download_with_wget(url, path)
|
12
|
+
else
|
13
|
+
logger.warn("failed: download #{url} -> #{path} (curl/wget not found)")
|
14
|
+
false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# downloads url and saves in path, using either curl or wget
|
19
|
+
# raises CommandFailed if download fails
|
20
|
+
def download!(url, path)
|
21
|
+
raise CommandFailed unless download(url, path)
|
22
|
+
end
|
23
|
+
|
24
|
+
# downloads url and saves to path, using curl
|
25
|
+
# logs success/failure message
|
26
|
+
def download_with_curl(url, path)
|
27
|
+
result = exec("curl -o #{path} -f #{url}")
|
28
|
+
if result.success?
|
29
|
+
logger.success("download #{url} -> #{path} (curl)")
|
30
|
+
true
|
31
|
+
else
|
32
|
+
logger.warn("failed: download #{url} -> #{path} (curl)")
|
33
|
+
false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def download_with_wget(url, path)
|
38
|
+
result = exec("wget -o #{path} -- #{url}")
|
39
|
+
if result.success?
|
40
|
+
logger.success("download #{url} -> #{path} (wget)")
|
41
|
+
true
|
42
|
+
else
|
43
|
+
logger.warn("failed: download #{url} -> #{path} (wget)")
|
44
|
+
false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
bangify :download_with_curl, :download_with_wget
|
49
|
+
|
50
|
+
end
|
51
|
+
end; end; end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Makitzo; module SSH; module Commands
|
2
|
+
module Makitzo
|
3
|
+
def makitzo_install
|
4
|
+
root = host.root!
|
5
|
+
if makitzo_install_check
|
6
|
+
logger.success("Makitzo already installed")
|
7
|
+
else
|
8
|
+
find_or_create_dir!(root, 'Makitzo root')
|
9
|
+
find_or_create_dir!(host.migration_history_dir, 'migration history directory')
|
10
|
+
exec!("echo COMPLETE > #{host.install_file}")
|
11
|
+
logger.success("Install successful")
|
12
|
+
end
|
13
|
+
logger.overall_success!
|
14
|
+
rescue CommandFailed => e
|
15
|
+
logger.error "installation aborted"
|
16
|
+
end
|
17
|
+
|
18
|
+
def makitzo_uninstall
|
19
|
+
root = host.root!
|
20
|
+
if root.length <= 1
|
21
|
+
logger.error "failsafe! I won't remove this directory: #{root}"
|
22
|
+
next
|
23
|
+
end
|
24
|
+
require_dir!(root, 'Makitzo root')
|
25
|
+
rm_rf!(root, 'Makitzo root')
|
26
|
+
logger.success("uninstall successful")
|
27
|
+
logger.overall_success!
|
28
|
+
rescue CommandFailed => e
|
29
|
+
logger.error "uninstallation aborted"
|
30
|
+
end
|
31
|
+
|
32
|
+
def makitzo_install_check
|
33
|
+
result = exec("cat #{host.install_file}")
|
34
|
+
return result.success? && result.stdout.strip == 'COMPLETE'
|
35
|
+
end
|
36
|
+
|
37
|
+
def makitzo_install_check!
|
38
|
+
unless makitzo_install_check
|
39
|
+
logger.error "Makitzo is not installed on this system"
|
40
|
+
raise CommandFailed
|
41
|
+
else
|
42
|
+
true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end; end; end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Makitzo; module SSH; module Commands
|
2
|
+
module Ruby
|
3
|
+
def ruby_version
|
4
|
+
ruby_version_check = exec("#{host.read_merged(:ruby_command) || 'ruby'} -v")
|
5
|
+
if ruby_version_check.error?
|
6
|
+
logger.warn "Ruby executable '#{host.ruby_command}' not found"
|
7
|
+
false
|
8
|
+
else
|
9
|
+
logger.success "Ruby executable located"
|
10
|
+
true
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def require_ruby!
|
15
|
+
raise CommandFailed unless ruby_version
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end; end; end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Makitzo; module SSH
|
2
|
+
class Context
|
3
|
+
def self.protected_context_methods
|
4
|
+
%w(x exec sudo host connection logger) + Migrations::Migration.protected_context_methods
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :host
|
8
|
+
attr_reader :connection
|
9
|
+
attr_accessor :connection_error
|
10
|
+
|
11
|
+
def initialize(host, connection)
|
12
|
+
@host, @connection = host, connection
|
13
|
+
end
|
14
|
+
|
15
|
+
def logger
|
16
|
+
@logger ||= (connection[:logger] || Logging::Blackhole.new)
|
17
|
+
end
|
18
|
+
|
19
|
+
# escape an argument for use in shell
|
20
|
+
# http://stackoverflow.com/questions/1306680/shellwords-shellescape-implementation-for-ruby-1-8
|
21
|
+
def x(arg)
|
22
|
+
arg = arg.strip
|
23
|
+
return "''" if arg.empty?
|
24
|
+
arg.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1")
|
25
|
+
arg.gsub!(/\n/, "'\n'")
|
26
|
+
arg
|
27
|
+
end
|
28
|
+
|
29
|
+
def quote(arg)
|
30
|
+
"#{x(arg)}"
|
31
|
+
end
|
32
|
+
|
33
|
+
# wrapper to connection.exec2!
|
34
|
+
# generates necessary sudo command if we're in a sudo block
|
35
|
+
# returns a status object with various useful data about command (output, status code)
|
36
|
+
def exec(command, options = {})
|
37
|
+
log_command = true
|
38
|
+
|
39
|
+
if @sudo
|
40
|
+
password = @sudo[:password] || host.read_merged(:sudo_password)
|
41
|
+
user = @sudo[:user]
|
42
|
+
group = @sudo[:group]
|
43
|
+
|
44
|
+
sudo = "sudo"
|
45
|
+
|
46
|
+
# TODO: if user/group is spec'd as int (ID), prefix it with #
|
47
|
+
sudo << " -u #{x(user)}" if user
|
48
|
+
sudo << " -g #{x(group)}" if group
|
49
|
+
|
50
|
+
log_sudo = sudo
|
51
|
+
|
52
|
+
if password
|
53
|
+
sudo = "echo #{x(password)} | #{sudo} -S --"
|
54
|
+
log_sudo = "echo [PASSWORD REMOVED] | #{log_sudo} -S --"
|
55
|
+
end
|
56
|
+
|
57
|
+
log_command = "#{log_sudo} #{command}"
|
58
|
+
command = "#{sudo} #{command}"
|
59
|
+
end
|
60
|
+
|
61
|
+
connection.exec2!(command, {:log => log_command}.update(options))
|
62
|
+
end
|
63
|
+
|
64
|
+
def exec!(command, options = {})
|
65
|
+
res = exec(command, options)
|
66
|
+
raise CommandFailed unless res.success?
|
67
|
+
end
|
68
|
+
|
69
|
+
def sudo(options = {})
|
70
|
+
raise "can't nest calls to sudo with different options" if (@sudo && (@sudo != options))
|
71
|
+
begin
|
72
|
+
@sudo = options
|
73
|
+
yield if block_given?
|
74
|
+
# reset sudo timestamp so password will be required next time
|
75
|
+
connection.exec2!("sudo -k")
|
76
|
+
ensure
|
77
|
+
@sudo = nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
include Commands::Apple
|
82
|
+
include Commands::FileSystem
|
83
|
+
include Commands::FileTransfer
|
84
|
+
include Commands::HTTP
|
85
|
+
include Commands::Ruby
|
86
|
+
include Commands::Unix
|
87
|
+
include Commands::Makitzo
|
88
|
+
|
89
|
+
include Migrations::Commands
|
90
|
+
end
|
91
|
+
end; end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Makitzo
|
2
|
+
module SSH
|
3
|
+
# mixing providing ability to run multiple SSH connections in parallel.
|
4
|
+
# clients mixing in this module should also include ApplicationAware,
|
5
|
+
# or provide +config+ and +logger+ methods.
|
6
|
+
module Multi
|
7
|
+
# connect to an array of hosts, returning an array of arrays.
|
8
|
+
# each array has the form [host, connection, error]
|
9
|
+
# only one of connection and error will be non-nil
|
10
|
+
def multi_connect(hosts, &block)
|
11
|
+
connection_threads = hosts.map { |h|
|
12
|
+
Thread.new do
|
13
|
+
begin
|
14
|
+
ssh_options = {}
|
15
|
+
|
16
|
+
password = h.read_merged(:ssh_password)
|
17
|
+
ssh_options[:password] = password if password
|
18
|
+
|
19
|
+
timeout = h.read_merged(:ssh_timeout)
|
20
|
+
ssh_options[:timeout] = timeout if timeout
|
21
|
+
|
22
|
+
[h, Net::SSH.start(h.name, h.read_merged(:ssh_username), ssh_options), nil]
|
23
|
+
rescue => e
|
24
|
+
[h, nil, e]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
}
|
28
|
+
connection_threads.map(&:value)
|
29
|
+
end
|
30
|
+
|
31
|
+
# execute a block on each host, in parallel
|
32
|
+
# block receives host, connection object and error object
|
33
|
+
# only one of connection, error will be non-nil
|
34
|
+
# returns after block has finished executing on all hosts
|
35
|
+
# returns array of block return values for each host
|
36
|
+
def multi_ssh(hosts, &block)
|
37
|
+
result = []
|
38
|
+
groups = config.concurrency.nil? ? [hosts] : (hosts.in_groups_of(config.concurrency))
|
39
|
+
groups.each do |hosts|
|
40
|
+
group_result = multi_connect(hosts).map { |host, conn, error|
|
41
|
+
conn[:logger] = logger if conn # ick?
|
42
|
+
Thread.new { block.call(host, conn, error) }
|
43
|
+
}.map(&:value)
|
44
|
+
group_result.each { |gr| result << gr }
|
45
|
+
end
|
46
|
+
result
|
47
|
+
end
|
48
|
+
|
49
|
+
def multi_session(hosts, &block)
|
50
|
+
context_klass = ssh_context_class
|
51
|
+
|
52
|
+
multi_ssh(hosts) do |host, conn, error|
|
53
|
+
context = context_klass.new(host, conn)
|
54
|
+
logger.with_host(host) do
|
55
|
+
if error
|
56
|
+
logger.error("could not connect to host: #{error.class}")
|
57
|
+
context.connection_error = error
|
58
|
+
else
|
59
|
+
begin
|
60
|
+
block.call(context, host)
|
61
|
+
rescue => e
|
62
|
+
logger.error("unhandled exception: #{e.class} (#{e.message})")
|
63
|
+
ensure
|
64
|
+
conn.close unless conn.closed?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
context
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def ssh_context_class
|
73
|
+
session_klass = Class.new(Makitzo::SSH::Context)
|
74
|
+
session_klass.send(:include, config.helpers)
|
75
|
+
session_klass
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require 'mysql2'
|
2
|
+
|
3
|
+
module Makitzo; module Store
|
4
|
+
class MySQL
|
5
|
+
attr_accessor :host, :port, :socket, :username, :password, :database
|
6
|
+
|
7
|
+
def initialize(config = {})
|
8
|
+
config.each { |k,v| send(:"#{k}=", v) }
|
9
|
+
@mutex = Mutex.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def open(&block)
|
13
|
+
connect!
|
14
|
+
begin
|
15
|
+
yield if block_given?
|
16
|
+
ensure
|
17
|
+
cleanup!
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def read(host, key)
|
22
|
+
sync do
|
23
|
+
@client.query("SELECT * FROM #{key_value_table} WHERE hostname = #{qs(host)} AND `key` = #{qs(key)}", :cast_booleans => true).each do |r|
|
24
|
+
return row_value(r)
|
25
|
+
end
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def write(host, key, value)
|
31
|
+
value_hash = {
|
32
|
+
'value_int' => 'NULL',
|
33
|
+
'value_float' => 'NULL',
|
34
|
+
'value_date' => 'NULL',
|
35
|
+
'value_datetime' => 'NULL',
|
36
|
+
'value_boolean' => 'NULL',
|
37
|
+
'value_string' => 'NULL'
|
38
|
+
}
|
39
|
+
|
40
|
+
case value
|
41
|
+
when Fixnum then value_hash['value_int'] = value.to_s
|
42
|
+
when Float then value_hash['value_float'] = value.to_s
|
43
|
+
when DateTime, Time then value_hash['value_datetime'] = qs(value.strftime("%Y-%m-%dT%H:%M:%S"))
|
44
|
+
when Date then value_hash['value_date'] = qs(value.strftime("%Y-%m-%d"))
|
45
|
+
when TrueClass, FalseClass then value_hash['value_boolean'] = value ? '1' : '0'
|
46
|
+
when NilClass then ; # do nothing
|
47
|
+
else value_hash['value_string'] = qs(value)
|
48
|
+
end
|
49
|
+
|
50
|
+
sync do
|
51
|
+
@client.query("
|
52
|
+
REPLACE INTO #{key_value_table}
|
53
|
+
(hostname, `key`, value_int, value_float, value_date, value_datetime, value_boolean, value_string)
|
54
|
+
VALUES
|
55
|
+
(#{qs(host)}, #{qs(key)}, #{value_hash['value_int']}, #{value_hash['value_float']},
|
56
|
+
#{value_hash['value_date']}, #{value_hash['value_datetime']}, #{value_hash['value_boolean']},
|
57
|
+
#{value_hash['value_string']})
|
58
|
+
")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def read_all(host, *keys)
|
63
|
+
out = [keys].flatten.inject({}) { |hsh,k| hsh[k.to_s] = nil; hsh }
|
64
|
+
sync do
|
65
|
+
@client.query("SELECT * FROM #{key_value_table} WHERE hostname = #{qs(host)} AND `key` IN (#{out.keys.map { |k| qs(k) }.join(', ')})", :cast_booleans => true).each do |r|
|
66
|
+
out[r['key']] = row_value(r)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
out
|
70
|
+
end
|
71
|
+
|
72
|
+
def write_all(host, hash)
|
73
|
+
sync do
|
74
|
+
hash.each { |k,v| write(host, k, v) }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def mark_migration_as_applied(host, migration)
|
79
|
+
sync do
|
80
|
+
@client.query("REPLACE INTO #{migrations_table} (hostname, migration_id) VALUES (#{qs(host)}, #{migration.to_i})")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def unmark_migration_as_applied(host, migration)
|
85
|
+
sync do
|
86
|
+
@client.query("DELETE FROM #{migrations_table} WHERE hostname = #{qs(host)} AND migration_id = #{migration.to_i}")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def applied_migrations_for_all_hosts
|
91
|
+
sync do
|
92
|
+
@client.query("SELECT * FROM #{migrations_table} ORDER BY migration_id ASC").inject({}) do |m,r|
|
93
|
+
(m[r['hostname']] ||= []) << r['migration_id']
|
94
|
+
m
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def applied_migrations_for_host(host)
|
100
|
+
sync do
|
101
|
+
@client.query("SELECT migration_id FROM #{migrations_table} WHERE hostname = #{qs(host)} ORDER BY migration_id ASC").inject([]) do |m,r|
|
102
|
+
m << r['migration_id']
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def sync(&block)
|
110
|
+
@mutex.synchronize(&block)
|
111
|
+
end
|
112
|
+
|
113
|
+
def row_value(r)
|
114
|
+
r['value_int'] || r['value_float'] || r['value_string'] || r['value_date'] || r['value_datetime'] || r['value_boolean']
|
115
|
+
end
|
116
|
+
|
117
|
+
def qs(str)
|
118
|
+
"'#{@client.escape(str.to_s)}'"
|
119
|
+
end
|
120
|
+
|
121
|
+
def connect!
|
122
|
+
@client = Mysql2::Client.new(connection_hash)
|
123
|
+
create_tables! unless tables_exist?
|
124
|
+
end
|
125
|
+
|
126
|
+
def cleanup!
|
127
|
+
@client.close if @client
|
128
|
+
end
|
129
|
+
|
130
|
+
def connection_hash
|
131
|
+
%w(host port socket username password database).inject({}) { |m,k| m[k.to_sym] = send(k); m }
|
132
|
+
end
|
133
|
+
|
134
|
+
def migrations_table
|
135
|
+
"makitzo_applied_migrations"
|
136
|
+
end
|
137
|
+
|
138
|
+
def key_value_table
|
139
|
+
"makitzo_key_values"
|
140
|
+
end
|
141
|
+
|
142
|
+
def tables_exist?
|
143
|
+
(@client.query("SHOW TABLES").map { |r| r.values.first } & [migrations_table, key_value_table]).length == 2
|
144
|
+
end
|
145
|
+
|
146
|
+
def create_tables!
|
147
|
+
@client.query("DROP TABLE IF EXISTS #{migrations_table}")
|
148
|
+
@client.query("DROP TABLE IF EXISTS #{key_value_table}")
|
149
|
+
|
150
|
+
sql = <<-SQL
|
151
|
+
CREATE TABLE `#{migrations_table}` (
|
152
|
+
`hostname` varchar(255) NOT NULL,
|
153
|
+
`migration_id` int(11) NOT NULL,
|
154
|
+
`hash` varchar(255) NULL,
|
155
|
+
PRIMARY KEY (`hostname`,`migration_id`)
|
156
|
+
) ENGINE=InnoDB
|
157
|
+
SQL
|
158
|
+
@client.query(sql)
|
159
|
+
|
160
|
+
sql = <<-SQL
|
161
|
+
CREATE TABLE `#{key_value_table}` (
|
162
|
+
`hostname` varchar(255) NOT NULL,
|
163
|
+
`key` varchar(255) NOT NULL,
|
164
|
+
`value_int` int(11) DEFAULT NULL,
|
165
|
+
`value_float` float DEFAULT NULL,
|
166
|
+
`value_string` varchar(255) DEFAULT NULL,
|
167
|
+
`value_date` date DEFAULT NULL,
|
168
|
+
`value_datetime` datetime DEFAULT NULL,
|
169
|
+
`value_boolean` tinyint(1) DEFAULT NULL,
|
170
|
+
PRIMARY KEY (`hostname`,`key`)
|
171
|
+
) ENGINE=InnoDB
|
172
|
+
SQL
|
173
|
+
@client.query(sql)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end; end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Makitzo; module Store
|
2
|
+
# Stores record persistent host state data including applied migrations and
|
3
|
+
# arbitrary key-value pairs.
|
4
|
+
#
|
5
|
+
# This is an interface definition for a store backend. It is not necessary to
|
6
|
+
# extend this class, but all methods must be implemented.
|
7
|
+
#
|
8
|
+
# All operations must raise ::Makitzo::Store::OperationFailedError on failure.
|
9
|
+
class Skeleton
|
10
|
+
def open(&block)
|
11
|
+
raise
|
12
|
+
end
|
13
|
+
|
14
|
+
def read(host, key)
|
15
|
+
raise
|
16
|
+
end
|
17
|
+
|
18
|
+
def write(host, key, value)
|
19
|
+
raise
|
20
|
+
end
|
21
|
+
|
22
|
+
def read_all(host, *keys)
|
23
|
+
raise
|
24
|
+
end
|
25
|
+
|
26
|
+
def write_all(host, hash)
|
27
|
+
raise
|
28
|
+
end
|
29
|
+
|
30
|
+
def mark_migration_as_applied(host, migration)
|
31
|
+
raise
|
32
|
+
end
|
33
|
+
|
34
|
+
def unmark_migration_as_applied(host, migration)
|
35
|
+
raise
|
36
|
+
end
|
37
|
+
|
38
|
+
def applied_migrations_for_all_hosts
|
39
|
+
raise
|
40
|
+
end
|
41
|
+
|
42
|
+
def applied_migrations_for_host(host)
|
43
|
+
raise
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end; end
|