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.
Files changed (49) hide show
  1. data/Gemfile +12 -0
  2. data/Gemfile.lock +27 -0
  3. data/LICENSE.txt +20 -0
  4. data/README.mdown +22 -0
  5. data/RESOURCES +2 -0
  6. data/Rakefile +51 -0
  7. data/VERSION +1 -0
  8. data/bin/makitzo +6 -0
  9. data/lib/makitzo/application.rb +151 -0
  10. data/lib/makitzo/application_aware.rb +19 -0
  11. data/lib/makitzo/cli.rb +71 -0
  12. data/lib/makitzo/config.rb +148 -0
  13. data/lib/makitzo/file_system.rb +24 -0
  14. data/lib/makitzo/logging/blackhole.rb +16 -0
  15. data/lib/makitzo/logging/collector.rb +170 -0
  16. data/lib/makitzo/logging/colorize.rb +38 -0
  17. data/lib/makitzo/memoized_proc.rb +15 -0
  18. data/lib/makitzo/migrations/commands.rb +11 -0
  19. data/lib/makitzo/migrations/generator.rb +24 -0
  20. data/lib/makitzo/migrations/migration.rb +69 -0
  21. data/lib/makitzo/migrations/migrator.rb +87 -0
  22. data/lib/makitzo/migrations/paths.rb +7 -0
  23. data/lib/makitzo/monkeys/array.rb +10 -0
  24. data/lib/makitzo/monkeys/bangify.rb +15 -0
  25. data/lib/makitzo/monkeys/net-ssh.rb +73 -0
  26. data/lib/makitzo/monkeys/string.rb +15 -0
  27. data/lib/makitzo/multiplexed_reader.rb +26 -0
  28. data/lib/makitzo/settings.rb +30 -0
  29. data/lib/makitzo/ssh/commands/apple.rb +104 -0
  30. data/lib/makitzo/ssh/commands/file_system.rb +59 -0
  31. data/lib/makitzo/ssh/commands/file_transfer.rb +9 -0
  32. data/lib/makitzo/ssh/commands/http.rb +51 -0
  33. data/lib/makitzo/ssh/commands/makitzo.rb +46 -0
  34. data/lib/makitzo/ssh/commands/ruby.rb +18 -0
  35. data/lib/makitzo/ssh/commands/unix.rb +7 -0
  36. data/lib/makitzo/ssh/context.rb +91 -0
  37. data/lib/makitzo/ssh/multi.rb +79 -0
  38. data/lib/makitzo/store/mysql.rb +176 -0
  39. data/lib/makitzo/store/skeleton.rb +46 -0
  40. data/lib/makitzo/world/host.rb +84 -0
  41. data/lib/makitzo/world/named_entity.rb +41 -0
  42. data/lib/makitzo/world/query.rb +54 -0
  43. data/lib/makitzo/world/role.rb +4 -0
  44. data/lib/makitzo.rb +90 -0
  45. data/makitzo.gemspec +106 -0
  46. data/templates/migration.erb +9 -0
  47. data/test/helper.rb +17 -0
  48. data/test/test_makitzo.rb +7 -0
  49. 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,7 @@
1
+ module Makitzo; module SSH; module Commands
2
+ module Unix
3
+ def killall(process_name)
4
+ exec("killall #{x(process_name)}")
5
+ end
6
+ end
7
+ 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