chef_backup 0.0.1.dev.2 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +5 -5
  2. data/lib/chef_backup.rb +8 -8
  3. data/lib/chef_backup/config.rb +22 -12
  4. data/lib/chef_backup/data_map.rb +23 -9
  5. data/lib/chef_backup/deep_merge.rb +145 -0
  6. data/lib/chef_backup/helpers.rb +169 -27
  7. data/lib/chef_backup/logger.rb +2 -2
  8. data/lib/chef_backup/mash.rb +226 -0
  9. data/lib/chef_backup/runner.rb +19 -14
  10. data/lib/chef_backup/strategy.rb +10 -10
  11. data/lib/chef_backup/strategy/backup/custom.rb +1 -2
  12. data/lib/chef_backup/strategy/backup/ebs.rb +3 -6
  13. data/lib/chef_backup/strategy/backup/lvm.rb +2 -4
  14. data/lib/chef_backup/strategy/backup/object.rb +2 -4
  15. data/lib/chef_backup/strategy/backup/tar.rb +116 -35
  16. data/lib/chef_backup/strategy/restore/tar.rb +103 -38
  17. data/lib/chef_backup/version.rb +1 -1
  18. metadata +22 -170
  19. data/.gitignore +0 -23
  20. data/.kitchen.yml +0 -30
  21. data/.rubocop.yml +0 -17
  22. data/.travis.yml +0 -6
  23. data/Gemfile +0 -4
  24. data/Guardfile +0 -22
  25. data/README.md +0 -21
  26. data/Rakefile +0 -44
  27. data/chef_backup.gemspec +0 -33
  28. data/spec/fixtures/chef-server-running.json +0 -584
  29. data/spec/spec_helper.rb +0 -103
  30. data/spec/unit/data_map_spec.rb +0 -59
  31. data/spec/unit/helpers_spec.rb +0 -88
  32. data/spec/unit/runner_spec.rb +0 -185
  33. data/spec/unit/shared_examples/helpers.rb +0 -20
  34. data/spec/unit/strategy/backup/lvm_spec.rb +0 -0
  35. data/spec/unit/strategy/backup/shared_examples/backup.rb +0 -74
  36. data/spec/unit/strategy/backup/tar_spec.rb +0 -294
  37. data/spec/unit/strategy/restore/lvm_spec.rb +0 -0
  38. data/spec/unit/strategy/restore/shared_examples/restore.rb +0 -84
  39. data/spec/unit/strategy/restore/tar_spec.rb +0 -238
  40. data/spec/unit/strategy_spec.rb +0 -36
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 77676069a215d7829c14023e70d43e71d27581a1
4
- data.tar.gz: e218d8428ec0edb82a440f9003db3035435e539d
2
+ SHA256:
3
+ metadata.gz: 1fd9e42aaddb3298dee25b5dff5ff972f92ed2b9554fac1cfdbd487edd3c9bc9
4
+ data.tar.gz: 3b53768ed83f86bcf25259e65a8b710198d3b97b9fdc7e5ca52446e7511e69d6
5
5
  SHA512:
6
- metadata.gz: e3850ccefb42090d4c73decaa3c9c661d50529aee9eb00a667bb97f648337871e2dafa9fbe1ad6c8cbb082f6ccb908a3d8799721da778c133402530fdb7c5d7c
7
- data.tar.gz: d89dc2fb4ef4bc7d95f907e1dec161cb26a0ff2749cee9e8cb1069e823c345d255a42227edd0cf48f638cbc2277d497fedc90689f95102b8f5f4a77599331708
6
+ metadata.gz: 3f9a37b97ab0553644d533a63d49b42fa68a28eef2f821da971f78341c5d1d4324f219ac9996ab8abb59f47d707e05e71fccbbee2003b9b0725260758ddb3d00
7
+ data.tar.gz: 8ad641f82c2e0592dd29eff51e8ebd155980bdd27730718f23cb798008c5ba226eadaeaba0764bf5e5305f614660885bc8605891629fbe72169aaf8bf718b18d
data/lib/chef_backup.rb CHANGED
@@ -2,11 +2,11 @@
2
2
  #
3
3
  # All Rights Reserved
4
4
 
5
- require 'chef_backup/version'
6
- require 'chef_backup/exceptions'
7
- require 'chef_backup/config'
8
- require 'chef_backup/logger'
9
- require 'chef_backup/data_map'
10
- require 'chef_backup/helpers'
11
- require 'chef_backup/runner'
12
- require 'chef_backup/strategy'
5
+ require "chef_backup/version"
6
+ require "chef_backup/exceptions"
7
+ require "chef_backup/config"
8
+ require "chef_backup/logger"
9
+ require "chef_backup/data_map"
10
+ require "chef_backup/helpers"
11
+ require "chef_backup/runner"
12
+ require "chef_backup/strategy"
@@ -1,18 +1,23 @@
1
- require 'fileutils'
2
- require 'json'
3
- require 'forwardable'
1
+ require "fileutils"
2
+ require "json"
3
+ require "forwardable"
4
4
 
5
5
  module ChefBackup
6
6
  # ChefBackup Global Config
7
7
  class Config
8
8
  extend Forwardable
9
9
 
10
+ DEFAULT_BASE = "private_chef".freeze
10
11
  DEFAULT_CONFIG = {
11
- 'backup' => {
12
- 'always_dump_db' => true,
13
- 'strategy' => 'none',
14
- 'export_dir' => '/var/opt/chef-backup'
15
- }
12
+ "backup" => {
13
+ "always_dump_db" => true,
14
+ "strategy" => "none",
15
+ "export_dir" => "/var/opt/chef-backup",
16
+ "project_name" => "opscode",
17
+ "ctl-command" => "chef-server-ctl",
18
+ "running_filepath" => "/etc/opscode/chef-server-running.json",
19
+ "database_name" => "opscode_chef",
20
+ },
16
21
  }.freeze
17
22
 
18
23
  class << self
@@ -45,13 +50,18 @@ module ChefBackup
45
50
  # @param config [Hash] a Hash of the private-chef-running.json
46
51
  #
47
52
  def initialize(config = {})
48
- config['private_chef'] ||= {}
49
- config['private_chef']['backup'] ||= {}
50
- config['private_chef']['backup'] =
51
- DEFAULT_CONFIG['backup'].merge(config['private_chef']['backup'])
53
+ config["config_base"] ||= DEFAULT_BASE
54
+ base = config["config_base"]
55
+ config[base] ||= {}
56
+ config[base]["backup"] ||= {}
57
+ config[base]["backup"] = DEFAULT_CONFIG["backup"].merge(config[base]["backup"])
52
58
  @config = config
53
59
  end
54
60
 
61
+ def to_hash
62
+ @config
63
+ end
64
+
55
65
  def_delegators :@config, :[], :[]=
56
66
  end
57
67
  end
@@ -1,4 +1,4 @@
1
- require 'time'
1
+ require "time"
2
2
 
3
3
  module ChefBackup
4
4
  # DataMap class to store data about the data we're backing up
@@ -11,33 +11,47 @@ module ChefBackup
11
11
  attr_writer :data_map
12
12
  end
13
13
 
14
- attr_accessor :strategy, :backup_time, :configs, :services
14
+ attr_accessor :strategy, :backup_time, :topology, :configs, :services, :ha, :versions
15
15
 
16
16
  def initialize
17
17
  @services = {}
18
18
  @configs = {}
19
+ @versions = {}
20
+ @ha = {}
19
21
  yield self if block_given?
20
22
 
21
23
  @backup_time ||= Time.now.iso8601
22
- @strategy ||= 'none'
24
+ @strategy ||= "none"
25
+ @toplogy ||= "idontknow"
23
26
  end
24
27
 
25
28
  def add_service(service, data_dir)
26
29
  @services[service] ||= {}
27
- @services[service]['data_dir'] = data_dir
30
+ @services[service]["data_dir"] = data_dir
28
31
  end
29
32
 
30
33
  def add_config(config, path)
31
34
  @configs[config] ||= {}
32
- @configs[config]['data_dir'] = path
35
+ @configs[config]["data_dir"] = path
36
+ end
37
+
38
+ def add_version(project_name, data)
39
+ @versions[project_name] = data
40
+ end
41
+
42
+ def add_ha_info(k, v)
43
+ @ha[k] = v
33
44
  end
34
45
 
35
46
  def manifest
36
47
  {
37
- 'strategy' => strategy,
38
- 'backup_time' => backup_time,
39
- 'services' => services,
40
- 'configs' => configs
48
+ "strategy" => strategy,
49
+ "backup_time" => backup_time,
50
+ "topology" => topology,
51
+ "ha" => ha,
52
+ "services" => services,
53
+ "configs" => configs,
54
+ "versions" => versions,
41
55
  }
42
56
  end
43
57
  end
@@ -0,0 +1,145 @@
1
+ #
2
+ # Author:: Adam Jacob (<adam@chef.io>)
3
+ # Author:: Steve Midgley (http://www.misuse.org/science)
4
+ # Copyright:: Copyright 2009-2016, Chef Software Inc.
5
+ # Copyright:: Copyright 2008-2016, Steve Midgley
6
+ # License:: Apache License, Version 2.0
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+ require 'chef_backup/mash'
20
+
21
+ #
22
+ # DANGER! THIS FILE WAS VENDORDED FROM CHEF. IF YOU ARE
23
+ # MAKING A CHANGE HERE, CONSIDER WHETHER IT IS REQUIRED IN CHEF ALSO?
24
+ #
25
+ module ChefBackup
26
+ module Mixin
27
+ # == Chef::Mixin::DeepMerge
28
+ # Implements a deep merging algorithm for nested data structures.
29
+ # ==== Notice:
30
+ # This code was originally imported from deep_merge by Steve Midgley.
31
+ # deep_merge is available under the MIT license from
32
+ # http://trac.misuse.org/science/wiki/DeepMerge
33
+ module DeepMerge
34
+
35
+ extend self
36
+
37
+ def merge(first, second)
38
+ first = Mash.new(first) unless first.kind_of?(Mash)
39
+ second = Mash.new(second) unless second.kind_of?(Mash)
40
+
41
+ DeepMerge.deep_merge(second, first)
42
+ end
43
+
44
+ class InvalidParameter < StandardError; end
45
+
46
+ # Deep Merge core documentation.
47
+ # deep_merge! method permits merging of arbitrary child elements. The two top level
48
+ # elements must be hashes. These hashes can contain unlimited (to stack limit) levels
49
+ # of child elements. These child elements to not have to be of the same types.
50
+ # Where child elements are of the same type, deep_merge will attempt to merge them together.
51
+ # Where child elements are not of the same type, deep_merge will skip or optionally overwrite
52
+ # the destination element with the contents of the source element at that level.
53
+ # So if you have two hashes like this:
54
+ # source = {:x => [1,2,3], :y => 2}
55
+ # dest = {:x => [4,5,'6'], :y => [7,8,9]}
56
+ # dest.deep_merge!(source)
57
+ # Results: {:x => [1,2,3,4,5,'6'], :y => 2}
58
+ # By default, "deep_merge!" will overwrite any unmergeables and merge everything else.
59
+ # To avoid this, use "deep_merge" (no bang/exclamation mark)
60
+ def deep_merge!(source, dest)
61
+ # if dest doesn't exist, then simply copy source to it
62
+ if dest.nil?
63
+ dest = source; return dest
64
+ end
65
+
66
+ case source
67
+ when nil
68
+ dest
69
+ when Hash
70
+ if dest.kind_of?(Hash)
71
+ source.each do |src_key, src_value|
72
+ if dest[src_key]
73
+ dest[src_key] = deep_merge!(src_value, dest[src_key])
74
+ else # dest[src_key] doesn't exist so we take whatever source has
75
+ dest[src_key] = src_value
76
+ end
77
+ end
78
+ else # dest isn't a hash, so we overwrite it completely
79
+ dest = source
80
+ end
81
+ when Array
82
+ if dest.kind_of?(Array)
83
+ dest = dest | source
84
+ else
85
+ dest = source
86
+ end
87
+ when String
88
+ dest = source
89
+ else # src_hash is not an array or hash, so we'll have to overwrite dest
90
+ dest = source
91
+ end
92
+ dest
93
+ end # deep_merge!
94
+
95
+ def hash_only_merge(merge_onto, merge_with)
96
+ hash_only_merge!(safe_dup(merge_onto), safe_dup(merge_with))
97
+ end
98
+
99
+ def safe_dup(thing)
100
+ thing.dup
101
+ rescue TypeError
102
+ thing
103
+ end
104
+
105
+ # Deep merge without Array merge.
106
+ # `merge_onto` is the object that will "lose" in case of conflict.
107
+ # `merge_with` is the object whose values will replace `merge_onto`s
108
+ # values when there is a conflict.
109
+ def hash_only_merge!(merge_onto, merge_with)
110
+ # If there are two Hashes, recursively merge.
111
+ if merge_onto.kind_of?(Hash) && merge_with.kind_of?(Hash)
112
+ merge_with.each do |key, merge_with_value|
113
+ value =
114
+ if merge_onto.has_key?(key)
115
+ hash_only_merge(merge_onto[key], merge_with_value)
116
+ else
117
+ merge_with_value
118
+ end
119
+
120
+ if merge_onto.respond_to?(:public_method_that_only_deep_merge_should_use)
121
+ # we can't call ImmutableMash#[]= because its immutable, but we need to mutate it to build it in-place
122
+ merge_onto.public_method_that_only_deep_merge_should_use(key, value)
123
+ else
124
+ merge_onto[key] = value
125
+ end
126
+ end
127
+ merge_onto
128
+
129
+ # If merge_with is nil, don't replace merge_onto
130
+ elsif merge_with.nil?
131
+ merge_onto
132
+
133
+ # In all other cases, replace merge_onto with merge_with
134
+ else
135
+ merge_with
136
+ end
137
+ end
138
+
139
+ def deep_merge(source, dest)
140
+ deep_merge!(safe_dup(source), safe_dup(dest))
141
+ end
142
+
143
+ end
144
+ end
145
+ end
@@ -1,35 +1,89 @@
1
1
  require 'fileutils'
2
+ require 'json'
2
3
  require 'mixlib/shellout'
3
4
  require 'chef_backup/config'
4
5
  require 'chef_backup/logger'
5
6
 
7
+ # rubocop:disable ModuleLength
6
8
  # rubocop:disable IndentationWidth
7
9
  module ChefBackup
8
10
  # Common helper methods that are usefull in many classes
9
11
  module Helpers
10
12
  # rubocop:enable IndentationWidth
11
13
 
12
- SERVER_ADD_ONS = %w(
13
- opscode-manage
14
- opscode-reporting
15
- opscode-push-jobs-server
16
- opscode-analytics
17
- chef-ha
18
- chef-sync
19
- ).freeze
14
+ SERVER_ADD_ONS = {
15
+ 'opscode-manage' => {
16
+ 'config_file' => '/etc/opscode-manage/manage.rb',
17
+ 'ctl_command' => 'opscode-manage-ctl'
18
+ },
19
+ 'opscode-reporting' => {
20
+ 'config_file' => '/etc/opscode-reporting/opscode-reporting.rb',
21
+ 'ctl_command' => 'opscode-reporting-ctl'
22
+ },
23
+ 'opscode-push-jobs-server' => {
24
+ 'config_file' => '/etc/opscode-push-jobs-server/opscode-push-jobs-server.rb',
25
+ 'ctl_command' => 'opscode-push-jobs-server-ctl'
26
+ },
27
+ 'opscode-analytics' => {
28
+ 'config_file' => '/etc/opscode-analytics/opscode-analytics.rb',
29
+ 'ctl_command' => 'opscode-analytics-ctl'
30
+ },
31
+ 'chef-ha' => {
32
+ 'config_file' => '/etc/opscode/chef-server.rb'
33
+ },
34
+ 'chef-sync' => {
35
+ 'config_file' => '/etc/chef-sync/chef-sync.rb',
36
+ 'ctl_command' => 'chef-sync-ctl'
37
+ },
38
+ 'chef-marketplace' => {
39
+ 'config_file' => '/etc/chef-marketplace/marketplace.rb',
40
+ 'ctl_command' => 'chef-marketplace-ctl'
41
+ }
42
+ }.freeze
20
43
 
21
- def private_chef
22
- config['private_chef']
23
- end
44
+ DEFAULT_PG_OPTIONS = '-c statement_timeout=3600000'.freeze
24
45
 
25
46
  def config
26
47
  ChefBackup::Config
27
48
  end
28
49
 
50
+ def config_base
51
+ ChefBackup::Config['config_base']
52
+ end
53
+
54
+ def service_config
55
+ ChefBackup::Config[config_base]
56
+ end
57
+
58
+ def ctl_command
59
+ service_config['backup']['ctl-command']
60
+ end
61
+
62
+ def running_filepath
63
+ service_config['backup']['running_filepath']
64
+ end
65
+
66
+ def database_name
67
+ service_config['backup']['database_name']
68
+ end
69
+
29
70
  def log(message, level = :info)
30
71
  ChefBackup::Logger.logger.log(message, level)
31
72
  end
32
73
 
74
+ # Note that when we are in the backup codepath, we have access to a running
75
+ # chef server and hence, the ctl command puts all our flags under the current
76
+ # running service namespace. The lets the default configuration of the server
77
+ # provide flags that the user doesn't necessarily provide on the command line.
78
+ #
79
+ # During the restore codepath, there may be no running chef server. This means
80
+ # that we need to be paranoid about the existence of the service_config hash.
81
+ def shell_timeout
82
+ option = config['shell_out_timeout'] ||
83
+ (service_config && service_config['backup']['shell_out_timeout'])
84
+ option.to_f unless option.nil?
85
+ end
86
+
33
87
  #
34
88
  # @param file [String] A path to a file on disk
35
89
  # @param exception [Exception] An exception to raise if file is not present
@@ -38,11 +92,13 @@ module Helpers
38
92
  # @return [TrueClass, FalseClass]
39
93
  #
40
94
  def ensure_file!(file, exception, message)
41
- File.exist?(file) ? true : fail(exception, message)
95
+ File.exist?(file) ? true : raise(exception, message)
42
96
  end
43
97
 
44
98
  def shell_out(*command)
45
- cmd = Mixlib::ShellOut.new(*command)
99
+ options = command.last.is_a?(Hash) ? command.pop : {}
100
+ opts_with_defaults = { 'timeout' => shell_timeout }.merge(options)
101
+ cmd = Mixlib::ShellOut.new(*command, opts_with_defaults)
46
102
  cmd.live_stream ||= $stdout.tty? ? $stdout : nil
47
103
  cmd.run_command
48
104
  cmd
@@ -54,8 +110,39 @@ module Helpers
54
110
  cmd
55
111
  end
56
112
 
113
+ def project_name
114
+ service_config['backup']['project_name']
115
+ end
116
+
117
+ def base_install_dir
118
+ "/opt/#{project_name}"
119
+ end
120
+
121
+ def addon_install_dir(name)
122
+ # can use extra field in SERVER_ADD_ONS to extend if someone isn't following this pattern.
123
+ "/opt/#{name}"
124
+ end
125
+
126
+ def base_config_dir
127
+ "/etc/#{project_name}"
128
+ end
129
+
130
+ def chpst
131
+ "#{base_install_dir}/embedded/bin/chpst"
132
+ end
133
+
134
+ def pgsql
135
+ "#{base_install_dir}/embedded/bin/psql"
136
+ end
137
+
138
+ def pg_options
139
+ config['pg_options'] ||
140
+ (service_config && service_config['backup']['pg_options']) ||
141
+ DEFAULT_PG_OPTIONS
142
+ end
143
+
57
144
  def all_services
58
- Dir['/opt/opscode/sv/*'].map { |f| File.basename(f) }.sort
145
+ Dir["#{base_install_dir}/sv/*"].map { |f| File.basename(f) }.sort
59
146
  end
60
147
 
61
148
  def enabled_services
@@ -63,20 +150,20 @@ module Helpers
63
150
  end
64
151
 
65
152
  def disabled_services
66
- all_services.select { |sv| !service_enabled?(sv) }
153
+ all_services.reject { |sv| service_enabled?(sv) }
67
154
  end
68
155
 
69
156
  def service_enabled?(service)
70
- File.symlink?("/opt/opscode/service/#{service}")
157
+ File.symlink?("#{base_install_dir}/service/#{service}")
71
158
  end
72
159
 
73
160
  def stop_service(service)
74
- res = shell_out("chef-server-ctl stop #{service}")
161
+ res = shell_out("#{ctl_command} stop #{service}")
75
162
  res
76
163
  end
77
164
 
78
165
  def start_service(service)
79
- res = shell_out("chef-server-ctl start #{service}")
166
+ res = shell_out("#{ctl_command} start #{service}")
80
167
  res
81
168
  end
82
169
 
@@ -92,34 +179,75 @@ module Helpers
92
179
  enabled_services.each { |sv| start_service(sv) }
93
180
  end
94
181
 
95
- def enabled_addons
96
- SERVER_ADD_ONS.select { |service| addon?(service) }
182
+ def restart_chef_server
183
+ shell_out("#{ctl_command} restart #{service}")
97
184
  end
98
185
 
99
- def addon?(service)
100
- File.directory?("/etc/#{service}")
186
+ def reconfigure_add_ons
187
+ enabled_addons.each do |_name, config|
188
+ shell_out("#{config['ctl_command']} reconfigure") if config.key?('ctl_command')
189
+ end
190
+ end
191
+
192
+ def restart_add_ons
193
+ enabled_addons.each do |_name, config|
194
+ shell_out("#{config['ctl_command']} restart") if config.key?('ctl_command')
195
+ end
196
+ end
197
+
198
+ def reconfigure_marketplace
199
+ log 'Setting up Chef Marketplace'
200
+ shell_out('chef-marketplace-ctl reconfigure')
201
+ end
202
+
203
+ def enabled_addons
204
+ SERVER_ADD_ONS.select do |name, config|
205
+ !config['config_file'].nil? &&
206
+ File.directory?(File.dirname(config['config_file'])) &&
207
+ File.directory?(addon_install_dir(name))
208
+ end
101
209
  end
102
210
 
103
211
  def strategy
104
- private_chef['backup']['strategy']
212
+ service_config['backup']['strategy']
213
+ end
214
+
215
+ def topology
216
+ service_config['topology']
105
217
  end
106
218
 
107
219
  def frontend?
108
- private_chef['role'] == 'frontend'
220
+ service_config['role'] == 'frontend'
109
221
  end
110
222
 
111
223
  def backend?
112
- private_chef['role'] =~ /backend|standalone/
224
+ service_config['role'] =~ /backend|standalone/
113
225
  end
114
226
 
115
227
  def online?
116
- private_chef['backup']['mode'] == 'online'
228
+ service_config['backup']['mode'] == 'online'
229
+ end
230
+
231
+ def ha?
232
+ topology == 'ha'
233
+ end
234
+
235
+ def tier?
236
+ topology == 'tier'
237
+ end
238
+
239
+ def standalone?
240
+ topology == 'standalone'
241
+ end
242
+
243
+ def marketplace?
244
+ shell_out('which chef-marketplace-ctl').exitstatus == 0
117
245
  end
118
246
 
119
247
  def tmp_dir
120
248
  @tmp_dir ||= begin
121
249
  dir = safe_key { config['tmp_dir'] } ||
122
- safe_key { private_chef['backup']['tmp_dir'] }
250
+ safe_key { service_config['backup']['tmp_dir'] }
123
251
  if dir
124
252
  FileUtils.mkdir_p(dir) unless File.directory?(dir)
125
253
  dir
@@ -136,6 +264,20 @@ module Helpers
136
264
  true
137
265
  end
138
266
 
267
+ def version_from_manifest_file(file)
268
+ return :no_version if file.nil?
269
+
270
+ path = File.expand_path(file)
271
+ if File.exist?(path)
272
+ config = JSON.parse(File.read(path))
273
+ { 'version' => config['build_version'],
274
+ 'revision' => config['build_git_revision'],
275
+ 'path' => path }
276
+ else
277
+ :no_version
278
+ end
279
+ end
280
+
139
281
  private
140
282
 
141
283
  def safe_key