makitzo 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|