chef-workflow 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ * 0.2.0 (unreleased)
2
+ * Several missing validation checks and edge races were resolved.
3
+ * Significant internals refactor -- things that were fast and simple when
4
+ things were small got less fast and simple later.
5
+ * Ruby's singleton library used instead of ugly hax
6
+ * Replace marshal system with a tie-alike database layer built atop sqlite
7
+ * Everything is namespaced under ChefWorkflow::
8
+ * Most of the things that break API above were marked deprecated and will print warnings if used.
9
+ * Provisioners now have a 'report' method which is used in informational
10
+ tasks to describe the provisioner's unique data.
11
+ * Fix for knife bootstrap actually DTRT in chef 10.18.x
12
+ * SSHHelper from testlib was moved here, now that it's used by tasklib as well.
13
+ * Docs. Lots and Lots and Lots of Docs.
14
+
15
+ * 0.1.1 December 21, 2012
16
+ * Fix gemspec. Here's to touching the stove.
17
+
18
+ * 0.1.0 December 21, 2012
19
+ * Initial public release
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012 Erik Hollensbe
1
+ Copyright (c) 2012, 2013 Erik Hollensbe
2
2
 
3
3
  MIT License
4
4
 
@@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
19
  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
20
  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
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.
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,145 +1,30 @@
1
1
  Chef Workflow Toolkit
2
2
  ---------------------
3
3
 
4
- This code is the common base of
5
- [chef-workflow-tasklib](https://github.com/chef-workflow/chef-workflow-tasklib)
6
- and
7
- [chef-workflow-testlib](https://github.com/chef-workflow/chef-workflow-testlib).
8
- Unless you are looking to create extensions to these two systems, or use the
9
- libraries contained within, you would be better served (with rare exception) by
10
- visiting these two projects for your testing and workflow needs.
11
-
12
- Environment Variables
13
- ---------------------
14
-
15
- This toolkit exposes a number of environment variables that you may want to set
16
- to affect your experience using it:
17
-
18
- * `CHEF_WORKFLOW_DEBUG` - set to an integer of 1-3, controls the amount of
19
- verbosity of reporting done in the libraries themselves. 1 usually amounts to
20
- diagnostic messages, 2 to full transactions (converges, bootstraps, VM
21
- provision detail), and 3 to more chatty individual pain points.
22
- * `CHEF_CONFIG` - the path to your knife.rb. Note that if you've set this up in
23
- `KnifeSupport` or with `configure_knife` you should almost never need this.
24
- * `TEST_CHEF_SUBNET` - specifies a /24 network that IP addresses are drawn from
25
- for local testing with VM systems. Note that this support is fairly crude --
26
- it's strongly suggested you use a proper /24 and make your last octet `0`.
27
- :)
28
-
29
- Classes
30
- -------
31
-
32
- Expect this to be supplemented with linked RDoc that describes the API for each
33
- class. For now, though, you'll have to generate the docs yourself or read the
34
- source comments.
35
-
36
- At the time of this writing there is not a very consistent namespacing method,
37
- expect this to be corrected before the first release.
38
-
39
- Utility libraries:
40
- ==================
41
-
42
- * `Chef::Workflow::ConfigureHelper` - a mixin which provides easy-to-use methods
43
- of driving the various support configuration systems (see below).
44
- * `AttrSupport` - a small mixin to supply chef-style instance variable mutators;
45
- the kind that are a little more convenient to use with `instance_eval`'d
46
- blocks.
47
- * `DebugSupport` - mixin which defines a method called `if_debug` which is our
48
- gating mechanism for the `CHEF_WORKFLOW_DEBUG` environment variable.
49
- * `GenericSupport` - mixin which keeps the configuration interface consistent by
50
- providing a `configure` class method that uses `instance_eval` and exposes a
51
- pre-configured object under `singleton` which can be manipulated.
52
- * `KnifePluginSupport` - mixin which contains a routine called
53
- `init_knife_plugin` to simplify configuration of knife plugins, which can
54
- then be used as normal objects. Also configures a UI object with `StringIO`
55
- so that it can be communicated with optionally.
56
-
57
- Configuration libraries:
58
- ========================
59
-
60
- These all mixin `GenericSupport` and as a result are expected to be treated as
61
- singletons, by accessing their `singleton` class method and configured with the
62
- `configure` class method which `instance_eval`'s a block.
63
-
64
- If you are using `chef-workflow-tasklib`, most of the bits here as you have
65
- configured them can be described to you via `bundle exec rake chef:show_config`.
66
-
67
- * `GeneralSupport` - "General" configuration attributes that are global to the
68
- entire system.
69
- * `IPSupport` - Database for associating IP addresses with a server group. See
70
- discussion on the scheduler below for more information on server groups. This
71
- is generally not configured externally, but by tooling within the system.
72
- * `KnifeSupport` - Most configuration regarding chef lives here, and additional
73
- network access.
74
- * `VagrantSupport` - Specific bits related to using Vagrant, such as the box to
75
- be used for provisioning.
76
- * `EC2Support` - bits related to driving AWS EC2.
77
-
78
- Scheduler and VM
79
- ----------------
80
-
81
- The Scheduler and VM system work together to make groups of machines easy to
82
- provision.
83
-
84
- The scheduler is responsible for scheduling provisioning of groups of machines
85
- which are interdependent with other machines. In other words when machine C
86
- depends on B and A to be provisioned, and B and A have no dependencies, and
87
- they are all scheduled at the same time, the scheduler will determine that A
88
- and B have to be provisioned immediately and as soon as they are provisioned
89
- successfully, it will attempt to provision C. Depending on the system
90
- controlling the scheduler and its constraints, it can do this in a serial or
91
- parallel fashion, the latter of which will attempt to provision as much as
92
- possible at the same time, and as things finish will provision things that are
93
- satisfied by what finished.
94
-
95
- In other words, provisioning takes a lot of time in a test run, and the
96
- scheduler tries very hard to make it take as little time as is reasonably
97
- possible given your constraints and the constraints of the system.
98
-
99
- It manages the actual act of provisioning through the VM system, which tracks
100
- the state of what's currently provisioned, what has already successfully
101
- provisioned (and presumed alive), and what is waiting to be provisoned. The VM
102
- class itself is largely responsible for exposing this data to the scheduler,
103
- and marshalling its state to disk in the event of a failure so things can be
104
- cleaned up or resumed in the case of resources that will always be depended on
105
- for a test run.
106
-
107
- In other words, provisioning takes a lot of time in a test run, and the VM
108
- system tries very hard to not add more time to this by tracking machines that
109
- are already provisioned so they don't have to be re-provisioned, even between
110
- runs. It also makes it easy to clean up a bad or stale test run.
111
-
112
- Server Groups
113
- =============
114
-
115
- The VM system itself is a mapping of server groups to an array of provisioning
116
- "commands", which are implemented as classes with a consistent interface
117
- (visitors). A provisioning command may create a [vagrant
118
- prison](https://github.com/chef-workflow/vagrant-prison) which contains all the
119
- servers for that server group, complete with assigning them a host-only
120
- interface and storing that with `IPSupport` so that it can be retrieved by
121
- other bits of the test system or task system. Another provisioning command may
122
- execute the in-code equivalent of `knife bootstrap` to build out your servers
123
- with a role named after the server group. For de-provisioning, the provisioning
124
- commands are played in reverse with a `shutdown` call applied to them.
125
-
126
- Scheduler and VM libraries
127
- ==========================
128
-
129
- * `Scheduler` - this is the meat; if you're driving a new testing system such
130
- as rspec, you'll want to get real familiar with the interface presented.
131
- * `VM` - marshalling and delegation interface. Most of this is exposed to the
132
- scheduler interface.
133
- * `VM::VagrantProvisioner` - creates a vagrant prison composed of n servers for
134
- a server group with a unique host-only ip for each server.
135
- * `VM::EC2Provisioner` - creates a group of servers provisioned on EC2.
136
- * `VM::KnifeProvisioner` - the provisioner equivalent of `knife bootstrap`, with
137
- additional sanity checks for both converge success and a waiting period for
138
- search indexing. On deprovision, deletes the nodes that were created. Will
139
- always try to bootstrap a group in parallel.
140
- * `VM::ChefServerProvisioner` - similar to `VM::KnifeProvisioner`, it wraps the
141
- [knife-server](https://github.com/fnichol/knife-server) toolkit to create a
142
- chef server.
4
+ A system to provide an encompassing toolkit and framework to work with chef
5
+ servers, their contents, and the networks of machines controlled by them. Your
6
+ test environment should look a lot like your production environment, and
7
+ chef-workflow lets you accomplish exactly that.
8
+
9
+ It is 3 major parts:
10
+
11
+ * This library, which is core functionality for unifying configuration of the
12
+ system, provisioning machines and maintaining a source of truth that lives
13
+ outside chef.
14
+ * [chef-workflow-tasklib](https://github.com/chef-workflow/chef-workflow-tasklib),
15
+ which is a toolkit that leverages `rake` to provide a common interface for
16
+ commanding chef-related operations.
17
+ * [chef-workflow-testlib](https://github.com/chef-workflow/chef-workflow-tasklib),
18
+ which is a toolkit for real-world integration testing. No mocks, real
19
+ machines, no bullshit.
20
+
21
+ Most of the Meat is on the Wiki
22
+ -------------------------------
23
+
24
+ Our [wiki](https://github.com/chef-workflow/chef-workflow/wiki) contains
25
+ a fair amount of information, including how to try chef-workflow without
26
+ actually doing anything more than cloning a repository and running a few
27
+ commands.
143
28
 
144
29
  Contributing
145
30
  ------------
@@ -156,6 +41,10 @@ without rationale will be rejected immediately.
156
41
  Credits
157
42
  -------
158
43
 
159
- This work was partially sponsored by [Hotel Tonight](http://hoteltonight.com)
160
- and is what they use to test our infrastructure internally. Primarily authored by
161
- [Erik Hollensbe](https://github.com/erikh).
44
+ Author: [Erik Hollensbe](https://github.com/erikh)
45
+
46
+ These companies have assisted by donating time, financial resources, and
47
+ employment to those working on chef-workflow. Supporting OSS is really really
48
+ cool and we should reciprocate.
49
+
50
+ * [HotelTonight](http://www.hoteltonight.com)
@@ -92,6 +92,12 @@ configure_knife do
92
92
  # hitting this limit a lot, increase this value.
93
93
  #
94
94
  # search_index_wait 60
95
+
96
+ # this is this list of recipes you want tested with the 'test:recipes' task.
97
+ # Note that you must have the 'minitest-handler' cookbook available on your
98
+ # chef server for these to work.
99
+ #
100
+ # test_recipes []
95
101
  end
96
102
 
97
103
  #
@@ -5,7 +5,7 @@ require 'chef-workflow/version'
5
5
 
6
6
  Gem::Specification.new do |gem|
7
7
  gem.name = "chef-workflow"
8
- gem.version = Chef::Workflow::VERSION
8
+ gem.version = ChefWorkflow::VERSION
9
9
  gem.authors = ["Erik Hollensbe"]
10
10
  gem.email = ["erik+github@hollensbe.org"]
11
11
  gem.description = %q{A comprehensive rake-based workflow for chef}
@@ -21,6 +21,9 @@ Gem::Specification.new do |gem|
21
21
  gem.add_dependency 'chef', '~> 10.0'
22
22
  gem.add_dependency 'aws-sdk', '~> 1.7.0'
23
23
  gem.add_dependency 'net-ssh', '~> 2.2.2'
24
+ gem.add_dependency 'knife-server', '~> 0.3.3'
25
+ gem.add_dependency 'sqlite3', '~> 1.3.6'
26
+ gem.add_dependency 'deprecated', '~> 3.0.1'
24
27
 
25
28
  gem.add_development_dependency 'rdoc'
26
29
  gem.add_development_dependency 'rake'
data/lib/chef-workflow.rb CHANGED
@@ -7,58 +7,61 @@ require 'chef-workflow/support/ip'
7
7
  require 'chef-workflow/support/debug'
8
8
  require 'chef-workflow/support/ec2'
9
9
 
10
- class Chef
11
- module Workflow
10
+ if ENV["REFACTOR"]
11
+ require 'deprecated'
12
+ Deprecated.set_action(:raise)
13
+ end
14
+
15
+ module ChefWorkflow
16
+ #
17
+ # Basic helpers (intended to be mixed in elsewhere) to configure the
18
+ # various support configuration systems.
19
+ #
20
+ module ConfigureHelper
21
+ #
22
+ # Configure 'GeneralSupport'
23
+ #
24
+ def configure_general(&block)
25
+ ChefWorkflow::GeneralSupport.configure(&block)
26
+ end
27
+
12
28
  #
13
- # Basic helpers (intended to be mixed in elsewhere) to configure the
14
- # various support configuration systems.
29
+ # Configure 'KnifeSupport'
15
30
  #
16
- module ConfigureHelper
17
- #
18
- # Configure 'GeneralSupport'
19
- #
20
- def configure_general(&block)
21
- GeneralSupport.configure(&block)
22
- end
31
+ def configure_knife(&block)
32
+ ChefWorkflow::KnifeSupport.configure(&block)
33
+ end
23
34
 
24
- #
25
- # Configure 'KnifeSupport'
26
- #
27
- def configure_knife(&block)
28
- KnifeSupport.configure(&block)
29
- end
35
+ #
36
+ # Configure 'VagrantSupport'
37
+ #
38
+ def configure_vagrant(&block)
39
+ ChefWorkflow::VagrantSupport.configure(&block)
40
+ end
30
41
 
31
- #
32
- # Configure 'VagrantSupport'
33
- #
34
- def configure_vagrant(&block)
35
- VagrantSupport.configure(&block)
36
- end
37
-
38
- #
39
- # Configure 'IPSupport' - you probably don't need to do this.
40
- #
41
- def configure_ips(&block)
42
- IPSupport.configure(&block)
43
- end
42
+ #
43
+ # Configure 'IPSupport' - you probably don't need to do this.
44
+ #
45
+ def configure_ips(&block)
46
+ ChefWorkflow::IPSupport.configure(&block)
47
+ end
44
48
 
45
- #
46
- # Configure 'EC2Support'
47
- #
48
- def configure_ec2(&block)
49
- EC2Support.configure(&block)
50
- end
49
+ #
50
+ # Configure 'EC2Support'
51
+ #
52
+ def configure_ec2(&block)
53
+ ChefWorkflow::EC2Support.configure(&block)
51
54
  end
52
55
  end
53
56
  end
54
57
 
55
58
  class << eval("self", TOPLEVEL_BINDING)
56
- include Chef::Workflow::ConfigureHelper
59
+ include ChefWorkflow::ConfigureHelper
57
60
  end
58
61
 
59
62
  if defined? Rake::DSL
60
63
  module Rake::DSL
61
- include Chef::Workflow::ConfigureHelper
64
+ include ChefWorkflow::ConfigureHelper
62
65
  end
63
66
  end
64
67
 
@@ -70,4 +73,3 @@ rescue LoadError
70
73
  $stderr.puts "There is no chef-workflow-config in your lib directory."
71
74
  $stderr.puts "Please run chef-workflow-bootstrap or add one."
72
75
  end
73
-
@@ -1,31 +1,33 @@
1
- #
2
- # Mixin to make exposing attr modification via `instance_eval` easier.
3
- #
4
- module AttrSupport
1
+ module ChefWorkflow
5
2
  #
6
- # Defines an attribute that is both a standard writer, but with an overloaded
7
- # reader that accepts an optional argument. Equivalent to this code for `foo`:
3
+ # Mixin to make exposing attr modification via `instance_eval` easier.
8
4
  #
9
- # attr_writer :foo
10
- #
11
- # def foo(*args)
12
- # if args.count > 0
13
- # @foo = arg
14
- # end
15
- #
16
- # @foo
17
- # end
18
- #
19
- def fancy_attr(name)
20
- class_eval <<-EOF
21
- attr_writer :#{name}
22
- def #{name}(*args)
23
- if args.count > 0
24
- @#{name} = args.first
25
- end
5
+ module AttrSupport
6
+ #
7
+ # Defines an attribute that is both a standard writer, but with an overloaded
8
+ # reader that accepts an optional argument. Equivalent to this code for `foo`:
9
+ #
10
+ # attr_writer :foo
11
+ #
12
+ # def foo(*args)
13
+ # if args.count > 0
14
+ # @foo = arg
15
+ # end
16
+ #
17
+ # @foo
18
+ # end
19
+ #
20
+ def fancy_attr(name)
21
+ class_eval <<-EOF
22
+ attr_writer :#{name}
23
+ def #{name}(*args)
24
+ if args.count > 0
25
+ @#{name} = args.first
26
+ end
26
27
 
27
- @#{name}
28
- end
29
- EOF
28
+ @#{name}
29
+ end
30
+ EOF
31
+ end
30
32
  end
31
33
  end
@@ -0,0 +1,26 @@
1
+ require 'sqlite3'
2
+ require 'fileutils'
3
+ require 'delegate'
4
+ require 'singleton'
5
+ require 'chef-workflow/support/general'
6
+
7
+ module ChefWorkflow
8
+ class DatabaseSupport < DelegateClass(SQLite3::Database)
9
+ include Singleton
10
+
11
+ def initialize
12
+ super(connect)
13
+ end
14
+
15
+ def reconnect
16
+ close rescue nil
17
+ __setobj__(connect)
18
+ end
19
+
20
+ def connect
21
+ vm_file = ChefWorkflow::GeneralSupport.vm_file
22
+ FileUtils.mkdir_p(File.dirname(vm_file))
23
+ SQLite3::Database.new(vm_file)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,225 @@
1
+ require 'chef-workflow/support/db'
2
+
3
+ module ChefWorkflow
4
+ class DatabaseSupport
5
+ class Generic
6
+ attr_accessor :db
7
+
8
+ def initialize(table_name, object_name=nil)
9
+ raise "a table_name must be provided!" unless table_name
10
+
11
+ @table_name = table_name
12
+ @object_name = object_name
13
+ post_marshal_init
14
+ create_table
15
+ end
16
+
17
+ def self._load(value)
18
+ obj = self.new(*Marshal.load(value))
19
+ return obj
20
+ end
21
+
22
+ def _dump(level)
23
+ self.db = nil
24
+ res = Marshal.dump([@table_name, @object_name])
25
+ post_marshal_init
26
+ return res
27
+ end
28
+
29
+ def post_marshal_init
30
+ @db = ChefWorkflow::DatabaseSupport.instance
31
+ end
32
+
33
+ def create_table
34
+ raise "Do not use the Generic type directly!"
35
+ end
36
+ end
37
+
38
+ class Object < Generic
39
+ def [](key)
40
+ value = @db.execute("select value from #{@table_name} where key=?", [key]).first.first rescue nil
41
+ return value && Marshal.load(value)
42
+ end
43
+
44
+ def []=(key, value)
45
+ delete(key)
46
+ @db.execute("insert into #{@table_name} (key, value) values (?, ?)", [key, Marshal.dump(value)])
47
+ value
48
+ end
49
+
50
+ def delete(key)
51
+ @db.execute("delete from #{@table_name} where key=?", [key])
52
+ end
53
+
54
+ def create_table
55
+ @db.execute <<-EOF
56
+ create table if not exists #{@table_name} (
57
+ id integer not null primary key autoincrement,
58
+ key varchar(255) not null,
59
+ value text not null,
60
+ UNIQUE(key)
61
+ )
62
+ EOF
63
+ end
64
+ end
65
+
66
+ class Collection < Generic
67
+ include Enumerable
68
+
69
+ def initialize(table_name, object_name)
70
+ raise "an object_name must be provided!" unless object_name
71
+ super
72
+ end
73
+ end
74
+
75
+ class List < Collection
76
+ def push(val)
77
+ @db.execute(
78
+ "insert into #{@table_name} (name, value) values (?, ?)",
79
+ [@object_name, Marshal.dump(val)]
80
+ )
81
+ end
82
+
83
+ alias << push
84
+
85
+ def unshift(val)
86
+ replace([val] + to_a)
87
+ end
88
+
89
+ def shift
90
+ to_a.shift
91
+ end
92
+
93
+ def pop
94
+ to_a.pop
95
+ end
96
+
97
+ def replace(ary)
98
+ clear
99
+
100
+ value_string = ("(?, ?)," * ary.count).chop
101
+
102
+ @db.execute(
103
+ "insert into #{@table_name} (name, value) values #{value_string}",
104
+ ary.map { |x| [@object_name, Marshal.dump(x)] }.flatten
105
+ )
106
+ end
107
+
108
+ def clear
109
+ @db.execute("delete from #{@table_name} where name=?", [@object_name])
110
+ end
111
+
112
+ def each
113
+ to_a.each { |x| yield x }
114
+ end
115
+
116
+ def to_a
117
+ @db.execute(
118
+ "select value from #{@table_name} where name=? order by id",
119
+ [@object_name]
120
+ ).map { |x| Marshal.load(x.first) }
121
+ end
122
+
123
+ def create_table
124
+ @db.execute <<-EOF
125
+ create table if not exists #{@table_name} (
126
+ id integer not null primary key autoincrement,
127
+ name varchar(255) not null,
128
+ value text not null
129
+ )
130
+ EOF
131
+
132
+ @db.execute "create index if not exists #{@table_name}_name_index on #{@table_name} (name)"
133
+ end
134
+ end
135
+
136
+ class Set < Collection
137
+ def add(key)
138
+ @db.execute("insert into #{@table_name} (name, key) values (?, ?)", [@object_name, key])
139
+ end
140
+
141
+ def delete(key)
142
+ @db.execute("delete from #{@table_name} where name=? and key=?", [@object_name, key])
143
+ end
144
+
145
+ def has_key?(key)
146
+ @db.execute("select count(*) from #{@table_name} where name=? and key=?", [@object_name, key]).first.first.to_i > 0
147
+ end
148
+
149
+ alias include? has_key?
150
+
151
+ def clear
152
+ @db.execute("delete from #{@table_name} where name=?", [@object_name])
153
+ end
154
+
155
+ def replace(set)
156
+ clear
157
+
158
+ return if set.empty?
159
+
160
+ value_string = ("(?, ?)," * set.count).chop
161
+
162
+ @db.execute("insert into #{@table_name} (name, key) values #{value_string}", set.map { |x| [@object_name, x] }.flatten)
163
+ end
164
+
165
+ def keys
166
+ @db.execute("select distinct key from #{@table_name} where name=?", [@object_name]).map(&:first)
167
+ end
168
+
169
+ def each
170
+ keys.each { |x| yield x }
171
+ end
172
+
173
+ def create_table
174
+ @db.execute <<-EOF
175
+ create table if not exists #{@table_name} (
176
+ id integer not null primary key autoincrement,
177
+ name varchar(255) not null,
178
+ key varchar(255) not null,
179
+ UNIQUE(name, key)
180
+ )
181
+ EOF
182
+
183
+ @db.execute "create index if not exists #{@table_name}_name_idx on #{@table_name} (name)"
184
+ end
185
+ end
186
+
187
+ class Map < Set
188
+ def [](key)
189
+ value = @db.execute("select value from #{@table_name} where name=? and key=?", [@object_name, key]).first.first rescue nil
190
+ return value && Marshal.load(value)
191
+ end
192
+
193
+ def []=(key, value)
194
+ delete(key)
195
+ @db.execute("insert into #{@table_name} (name, key, value) values (?, ?, ?)", [@object_name, key, Marshal.dump(value)])
196
+ value
197
+ end
198
+
199
+ def each
200
+ keys.each do |key|
201
+ yield key, self[key]
202
+ end
203
+ end
204
+
205
+ def to_hash
206
+ rows = @db.execute("select key, value from #{@table_name} where name=?", [@object_name])
207
+ Hash[rows.map { |x| [x[0], Marshal.load(x[1])] }]
208
+ end
209
+
210
+ def create_table
211
+ @db.execute <<-EOF
212
+ create table if not exists #{@table_name} (
213
+ id integer not null primary key autoincrement,
214
+ name varchar(255) not null,
215
+ key varchar(255) not null,
216
+ value text not null,
217
+ UNIQUE(name, key)
218
+ )
219
+ EOF
220
+
221
+ @db.execute "create index if not exists #{@table_name}_name_idx on #{@table_name} (name)"
222
+ end
223
+ end
224
+ end
225
+ end