makitzo 0.1.2

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