knife-ec-backup 2.0.0.beta.2 → 2.0.0.beta.3

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.
@@ -0,0 +1,148 @@
1
+ #
2
+ # Author:: Steven Danna (<steve@getchef.com>)
3
+ # Copyright:: Copyright (c) 2014 Chef Software, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'chef/knife'
20
+
21
+ class Chef
22
+ class Knife
23
+ module EcBase
24
+ class NoAdminFound < Exception; end
25
+
26
+ def self.included(includer)
27
+ includer.class_eval do
28
+
29
+ option :concurrency,
30
+ :long => '--concurrency THREADS',
31
+ :description => 'Maximum number of simultaneous requests to send (default: 10)'
32
+
33
+ option :webui_key,
34
+ :long => '--webui-key KEYPATH',
35
+ :description => 'Path to the WebUI Key (default: /etc/opscode/webui_priv.pem)',
36
+ :default => '/etc/opscode/webui_priv.pem'
37
+
38
+ option :skip_useracl,
39
+ :long => '--skip-useracl',
40
+ :boolean => true,
41
+ :default => false,
42
+ :description => "Skip downloading/restoring User ACLs. This is required for EC 11.0.2 and lower"
43
+
44
+ option :skip_version,
45
+ :long => '--skip-version-check',
46
+ :boolean => true,
47
+ :default => false,
48
+ :description => "Skip Chef Server version check. This will also skip any auto-configured options"
49
+
50
+ option :org,
51
+ :long => "--only-org ORG",
52
+ :description => "Only download/restore objects in the named organization (default: all orgs)"
53
+
54
+ option :sql_host,
55
+ :long => '--sql-host HOSTNAME',
56
+ :description => 'Postgresql database hostname (default: localhost)',
57
+ :default => "localhost"
58
+
59
+ option :sql_port,
60
+ :long => '--sql-port PORT',
61
+ :description => 'Postgresql database port (default: 5432)',
62
+ :default => 5432
63
+
64
+ option :sql_user,
65
+ :long => "--sql-user USERNAME",
66
+ :description => 'User used to connect to the postgresql database.'
67
+
68
+ option :sql_password,
69
+ :long => "--sql-password PASSWORD",
70
+ :description => 'Password used to connect to the postgresql database'
71
+
72
+ option :with_user_sql,
73
+ :long => '--with-user-sql',
74
+ :description => 'Try direct data base access for user export/import. Required to properly handle passwords, keys, and USAGs'
75
+ end
76
+
77
+ attr_accessor :dest_dir
78
+
79
+ def configure_chef
80
+ super
81
+ Chef::Config[:concurrency] = config[:concurrency].to_i if config[:concurrency]
82
+ Chef::ChefFS::Parallelizer.threads = (Chef::Config[:concurrency] || 10) - 1
83
+ end
84
+
85
+ def org_admin
86
+ rest = Chef::REST.new(Chef::Config.chef_server_url)
87
+ admin_users = rest.get_rest('groups/admins')['users']
88
+ org_members = rest.get_rest('users').map { |user| user['user']['username'] }
89
+ admin_users.delete_if { |user| !org_members.include?(user) || user == 'pivotal' }
90
+ if admin_users.empty?
91
+ raise Chef::Knife::EcBase::NoAdminFound
92
+ else
93
+ admin_users[0]
94
+ end
95
+ end
96
+ end
97
+
98
+ def server
99
+ @server ||= if Chef::Config.chef_server_root.nil?
100
+ ui.warn("chef_server_root not found in knife configuration; using chef_server_url")
101
+ Chef::Server.from_chef_server_url(Chef::Config.chef_server_url)
102
+ else
103
+ Chef::Server.new(Chef::Config.chef_server_root)
104
+ end
105
+ end
106
+
107
+ def rest
108
+ @rest ||= Chef::REST.new(server.root_url)
109
+ end
110
+
111
+ def user_acl_rest
112
+ @user_acl_rest ||= if config[:skip_version]
113
+ rest
114
+ elsif server.supports_user_acls?
115
+ rest
116
+ elsif server.direct_account_access?
117
+ Chef::REST.new("http://127.0.0.1:9465")
118
+ end
119
+
120
+ end
121
+
122
+ def set_skip_user_acl!
123
+ config[:skip_useracl] ||= !(server.supports_user_acls? || server.direct_account_access?)
124
+ end
125
+
126
+ def set_client_config!
127
+ Chef::Config.custom_http_headers = (Chef::Config.custom_http_headers || {}).merge({'x-ops-request-source' => 'web'})
128
+ Chef::Config.node_name = 'pivotal'
129
+ Chef::Config.client_key = config[:webui_key]
130
+ end
131
+
132
+ def set_dest_dir_from_args!
133
+ if name_args.length <= 0
134
+ ui.error("Must specify backup directory as an argument.")
135
+ exit 1
136
+ end
137
+ @dest_dir = name_args[0]
138
+ end
139
+
140
+ def ensure_webui_key_exists!
141
+ if !File.exist?(config[:webui_key])
142
+ ui.error("Webui Key (#{config[:webui_key]}) does not exist.")
143
+ exit 1
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -1,17 +1,13 @@
1
1
  require 'chef/knife'
2
+ require 'chef/knife/ec_base'
2
3
 
3
4
  class Chef
4
5
  class Knife
5
6
  class EcRestore < Chef::Knife
6
- banner "knife ec restore DIRECTORY"
7
7
 
8
- option :concurrency,
9
- :long => '--concurrency THREADS',
10
- :description => 'Maximum number of simultaneous requests to send (default: 10)'
8
+ include Knife::EcBase
11
9
 
12
- option :webui_key,
13
- :long => '--webui-key KEYPATH',
14
- :description => 'Used to set the path to the WebUI Key (default: /etc/opscode/webui_priv.pem)'
10
+ banner "knife ec restore DIRECTORY"
15
11
 
16
12
  option :overwrite_pivotal,
17
13
  :long => '--overwrite-pivotal',
@@ -19,48 +15,10 @@ class Chef
19
15
  :default => false,
20
16
  :description => "Whether to overwrite pivotal's key. Once this is done, future requests will fail until you fix the private key."
21
17
 
22
- option :skip_useracl,
23
- :long => '--skip-useracl',
24
- :boolean => true,
25
- :default => false,
26
- :description => "Whether to skip restoring User ACLs. This is required for EC 11.0.2 and lower"
27
-
28
- option :skip_version,
29
- :long => '--skip-version-check',
30
- :boolean => true,
31
- :default => false,
32
- :description => "Whether to skip checking the Chef Server version. This will also skip any auto-configured options"
33
-
34
- option :org,
35
- :long => "--only-org ORG",
36
- :description => "Only restore objects in the named organization (default: all orgs)"
37
-
38
18
  option :skip_users,
39
19
  :long => "--skip-users",
40
20
  :description => "Skip restoring users"
41
21
 
42
- option :with_user_sql,
43
- :long => "--with-user-sql",
44
- :description => "Restore user id's, passwords, and keys from sql export"
45
-
46
- option :sql_host,
47
- :long => '--sql-host HOSTNAME',
48
- :description => 'Postgresql database hostname (default: localhost)',
49
- :default => "localhost"
50
-
51
- option :sql_port,
52
- :long => '--sql-port PORT',
53
- :description => 'Postgresql database port (default: 5432)',
54
- :default => 5432
55
-
56
- option :sql_user,
57
- :long => "--sql-user USERNAME",
58
- :description => 'User used to connect to the postgresql database.'
59
-
60
- option :sql_password,
61
- :long => "--sql-password PASSWORD",
62
- :description => 'Password used to connect to the postgresql database'
63
-
64
22
  deps do
65
23
  require 'chef/json_compat'
66
24
  require 'chef/chef_fs/config'
@@ -73,172 +31,104 @@ class Chef
73
31
  require 'securerandom'
74
32
  require 'chef/chef_fs/parallelizer'
75
33
  require 'chef/tsorter'
76
- end
77
-
78
- def configure_chef
79
- super
80
- Chef::Config[:concurrency] = config[:concurrency].to_i if config[:concurrency]
81
- Chef::ChefFS::Parallelizer.threads = (Chef::Config[:concurrency] || 10) - 1
34
+ require 'chef/server'
82
35
  end
83
36
 
84
37
  def run
85
- #Check for destination directory argument
86
- if name_args.length <= 0
87
- ui.error("Must specify backup directory as an argument.")
88
- exit 1
89
- end
90
- dest_dir = name_args[0]
91
-
92
- #Check for pivotal user and key
93
- node_name = Chef::Config.node_name
94
- client_key = Chef::Config.client_key
95
- if node_name != "pivotal"
96
- if !File.exist?("/etc/opscode/pivotal.pem")
97
- ui.error("Username not configured as pivotal and /etc/opscode/pivotal.pem does not exist. It is recommended that you run this plugin from your Chef server.")
98
- exit 1
99
- end
100
- Chef::Config.node_name = 'pivotal'
101
- Chef::Config.client_key = '/etc/opscode/pivotal.pem'
38
+ set_dest_dir_from_args!
39
+ set_client_config!
40
+ ensure_webui_key_exists!
41
+ set_skip_user_acl!
42
+
43
+ restore_users unless config[:skip_users]
44
+ restore_user_sql if config[:with_user_sql]
45
+
46
+ for_each_organization do |orgname|
47
+ create_organization(orgname)
48
+ restore_open_invitations(orgname)
49
+ add_users_to_org(orgname)
50
+ upload_org_data(orgname)
102
51
  end
103
52
 
104
- #Check for WebUI Key
105
- if config[:webui_key] == nil
106
- if !File.exist?("/etc/opscode/webui_priv.pem")
107
- ui.error("WebUI not specified and /etc/opscode/webui_priv.pem does not exist. It is recommended that you run this plugin from your Chef server.")
108
- exit 1
109
- end
110
- ui.warn("WebUI not specified. Using /etc/opscode/webui_priv.pem")
111
- webui_key = '/etc/opscode/webui_priv.pem'
53
+ if config[:skip_useracl]
54
+ ui.warn("Skipping user ACL update. To update user ACLs, remove --skip-useracl or upgrade your Enterprise Chef Server.")
112
55
  else
113
- webui_key = config[:webui_key]
56
+ restore_user_acls
114
57
  end
58
+ end
115
59
 
116
- #Set the server root
117
- server_root = Chef::Config.chef_server_root
118
- if server_root == nil
119
- server_root = Chef::Config.chef_server_url.gsub(/\/organizations\/+[^\/]+\/*$/, '')
120
- ui.warn("chef_server_root not found in knife configuration. Setting root to: #{server_root}")
121
- Chef::Config.chef_server_root = server_root
60
+ def create_organization(orgname)
61
+ org = JSONCompat.from_json(File.read("#{dest_dir}/organizations/#{orgname}/org.json"))
62
+ rest.post_rest('organizations', org)
63
+ rescue Net::HTTPServerException => e
64
+ if e.response.code == "409"
65
+ rest.put_rest("organizations/#{orgname}", org)
66
+ else
67
+ raise
122
68
  end
69
+ end
123
70
 
124
- if config[:skip_version] && config[:skip_useracl]
125
- ui.warn("Skipping the Chef Server version check. This will also skip any auto-configured options")
126
- user_acl_rest = nil
127
- elsif config[:skip_version] && !config[:skip_useracl]
128
- ui.warn("Skipping the Chef Server version check. This will also skip any auto-configured options")
129
- user_acl_rest = rest
130
- else # Grab Chef Server version number so that we can auto set options
131
- uri = URI.parse("#{Chef::Config.chef_server_root}/version")
132
- server_version = open(uri, {ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE}).each_line.first.split(' ').last
133
- server_version_parts = server_version.split('.')
134
-
135
- if server_version_parts.count == 3
136
- puts "Detected Enterprise Chef Server version: #{server_version}"
137
-
138
- # All versions of Chef Server below 11.0.X are unable to update user acls
139
- if server_version_parts[0].to_i < 11 || (server_version_parts[0].to_i == 11 && server_version_parts[1].to_i == 0)
140
- ui.warn("Your version of Enterprise Chef Server does not support the updating of User ACLs. Setting skip-useracl to TRUE")
141
- config[:skip_useracl] = true
142
- user_acl_rest = nil
143
- else
144
- user_acl_rest = rest
71
+ def restore_open_invitations(orgname)
72
+ invitations = JSONCompat.from_json(File.read("#{dest_dir}/organizations/#{orgname}/invitations.json"))
73
+ invitations.each do |invitation|
74
+ begin
75
+ rest.post_rest("organizations/#{orgname}/association_requests", { 'user' => invitation['username'] })
76
+ rescue Net::HTTPServerException => e
77
+ if e.response.code != "409"
78
+ ui.error("Cannot create invitation #{invitation['id']}")
145
79
  end
146
- else
147
- ui.warn("Unable to detect Chef Server version.")
148
80
  end
149
81
  end
82
+ end
150
83
 
151
- rest = Chef::REST.new(Chef::Config.chef_server_root)
152
- # Restore users
153
- restore_users(dest_dir, rest) unless config[:skip_users]
154
- restore_user_sql(dest_dir) if config[:with_user_sql]
155
-
156
- # Restore organizations
157
- Dir.foreach("#{dest_dir}/organizations") do |name|
158
- next if name == '..' || name == '.' || !File.directory?("#{dest_dir}/organizations/#{name}")
159
- next unless (config[:org].nil? || config[:org] == name)
160
- puts "Restoring org #{name} ..."
161
-
162
- # Create organization
163
- org = JSONCompat.from_json(IO.read("#{dest_dir}/organizations/#{name}/org.json"))
84
+ def add_users_to_org(orgname)
85
+ members = JSONCompat.from_json(File.read("#{dest_dir}/organizations/#{orgname}/members.json"))
86
+ members.each do |member|
87
+ username = member['user']['username']
164
88
  begin
165
- rest.post_rest('organizations', org)
89
+ response = rest.post_rest("organizations/#{orgname}/association_requests", { 'user' => username })
90
+ association_id = response["uri"].split("/").last
91
+ rest.put_rest("users/#{username}/association_requests/#{association_id}", { 'response' => 'accept' })
166
92
  rescue Net::HTTPServerException => e
167
- if e.response.code == "409"
168
- rest.put_rest("organizations/#{name}", org)
169
- else
93
+ if e.response.code != "409"
170
94
  raise
171
95
  end
172
96
  end
97
+ end
98
+ end
173
99
 
174
- # Restore open invitations
175
- invitations = JSONCompat.from_json(IO.read("#{dest_dir}/organizations/#{name}/invitations.json"))
176
- invitations.each do |invitation|
177
- begin
178
- rest.post_rest("organizations/#{name}/association_requests", { 'user' => invitation['username'] })
179
- rescue Net::HTTPServerException => e
180
- if e.response.code != "409"
181
- ui.error("Cannot create invitation #{invitation['id']}")
182
- end
183
- end
184
- end
185
-
186
- # Repopulate org members
187
- members = JSONCompat.from_json(IO.read("#{dest_dir}/organizations/#{name}/members.json"))
188
- members.each do |member|
189
- username = member['user']['username']
190
- begin
191
- response = rest.post_rest("organizations/#{name}/association_requests", { 'user' => username })
192
- association_id = response["uri"].split("/").last
193
- rest.put_rest("users/#{username}/association_requests/#{association_id}", { 'response' => 'accept' })
194
- rescue Net::HTTPServerException => e
195
- if e.response.code != "409"
196
- raise
197
- end
198
- end
199
- end
200
-
201
- # Upload org data
202
- upload_org(dest_dir, webui_key, name)
100
+ def restore_user_acls
101
+ ui.msg "Restoring user ACLs ..."
102
+ for_each_user do |name|
103
+ user_acl = JSONCompat.from_json(File.read("#{dest_dir}/user_acls/#{name}.json"))
104
+ put_acl(user_acl_rest, "users/#{name}/_acl", user_acl)
203
105
  end
106
+ end
204
107
 
205
- # Restore user ACLs
206
- puts "Restoring user ACLs ..."
108
+ def for_each_user
207
109
  Dir.foreach("#{dest_dir}/users") do |filename|
208
110
  next if filename !~ /(.+)\.json/
209
111
  name = $1
210
- if config[:skip_useracl]
211
- ui.warn("Skipping user ACL update for #{name}. To update this ACL, remove --skip-useracl or upgrade your Enterprise Chef Server.")
212
- next
213
- end
214
112
  if name == 'pivotal' && !config[:overwrite_pivotal]
215
- ui.warn("Skipping pivotal update. To overwrite pivotal, pass --overwrite-pivotal.")
113
+ ui.warn("Skipping pivotal user. To overwrite pivotal, pass --overwrite-pivotal.")
216
114
  next
217
115
  end
218
-
219
- # Update user acl
220
- user_acl = JSONCompat.from_json(IO.read("#{dest_dir}/user_acls/#{name}.json"))
221
- put_acl(rest, "users/#{name}/_acl", user_acl)
116
+ yield name
222
117
  end
118
+ end
223
119
 
224
-
225
- if @error
226
- exit 1
120
+ def for_each_organization
121
+ Dir.foreach("#{dest_dir}/organizations") do |name|
122
+ next if name == '..' || name == '.' || !File.directory?("#{dest_dir}/organizations/#{name}")
123
+ next unless (config[:org].nil? || config[:org] == name)
124
+ yield name
227
125
  end
228
126
  end
229
127
 
230
- def restore_users(dest_dir, rest)
231
- puts "Restoring users ..."
232
- Dir.foreach("#{dest_dir}/users") do |filename|
233
- next if filename !~ /(.+)\.json/
234
- name = $1
235
- if name == 'pivotal' && !config[:overwrite_pivotal]
236
- ui.warn("Skipping pivotal update. To overwrite pivotal, pass --overwrite-pivotal.")
237
- next
238
- end
239
-
240
- # Update user object
241
- user = JSONCompat.from_json(IO.read("#{dest_dir}/users/#{name}.json"))
128
+ def restore_users
129
+ ui.msg "Restoring users ..."
130
+ for_each_user do |name|
131
+ user = JSONCompat.from_json(File.read("#{dest_dir}/users/#{name}.json"))
242
132
  begin
243
133
  # Supply password for new user
244
134
  user_with_password = user.dup
@@ -254,7 +144,7 @@ class Chef
254
144
  end
255
145
  end
256
146
 
257
- def restore_user_sql(dest_dir)
147
+ def restore_user_sql
258
148
  require 'chef/knife/ec_key_import'
259
149
  k = Chef::Knife::EcKeyImport.new
260
150
  k.name_args = ["#{dest_dir}/key_dump.json"]
@@ -268,7 +158,7 @@ class Chef
268
158
  end
269
159
 
270
160
  PATHS = %w(chef_repo_path cookbook_path environment_path data_bag_path role_path node_path client_path acl_path group_path container_path)
271
- def upload_org(dest_dir, webui_key, name)
161
+ def upload_org_data(name)
272
162
  old_config = Chef::Config.save
273
163
 
274
164
  begin
@@ -279,40 +169,42 @@ class Chef
279
169
 
280
170
  Chef::Config.chef_repo_path = "#{dest_dir}/organizations/#{name}"
281
171
  Chef::Config.versioned_cookbooks = true
282
-
283
- Chef::Config.chef_server_url = "#{Chef::Config.chef_server_root}/organizations/#{name}"
172
+ Chef::Config.chef_server_url = "#{server.root_url}/organizations/#{name}"
284
173
 
285
174
  # Upload the admins group and billing-admins acls
286
- puts "Restoring the org admin data"
175
+ ui.msg "Restoring the org admin data"
287
176
  chef_fs_config = Chef::ChefFS::Config.new
288
177
 
289
- # Restore users w/o clients (which don't exist yet)
178
+ # Handle Admins and Billing Admins seperately
179
+ #
180
+ # admins: We need to upload admins first so that we
181
+ # can upload all of the other objects as a user in the org
182
+ # rather than as pivotal. Because the clients, and groups, don't
183
+ # exist yet, we first upload the group with only the users.
184
+ #
185
+ # billing-admins: The default permissions on the
186
+ # billing-admin group only give update permissions to
187
+ # pivotal and members of the billing-admins group. Since we
188
+ # can't unsure that the admin we choose for uploading will
189
+ # be in the billing admins group, we have to upload this
190
+ # group as pivotal. Thus, we upload its users and ACL here,
191
+ # and then update it again once all of the clients and
192
+ # groups are uploaded.
193
+ #
290
194
  ['admins', 'billing-admins'].each do |group|
291
195
  restore_group(chef_fs_config, group, :clients => false)
292
196
  end
293
197
 
294
198
  pattern = Chef::ChefFS::FilePattern.new('/acls/groups/billing-admins.json')
295
- if Chef::ChefFS::FileSystem.copy_to(pattern, chef_fs_config.local_fs, chef_fs_config.chef_fs, nil, config, ui, proc { |entry| chef_fs_config.format_path(entry) })
296
- @error = true
297
- end
199
+ Chef::ChefFS::FileSystem.copy_to(pattern, chef_fs_config.local_fs,
200
+ chef_fs_config.chef_fs, nil, config, ui,
201
+ proc { |entry| chef_fs_config.format_path(entry)})
298
202
 
299
- # Figure out who the admin is so we can spoof him and retrieve his stuff
300
- rest = Chef::REST.new(Chef::Config.chef_server_url)
301
- org_admins = rest.get_rest('groups/admins')['users']
302
- org_members = rest.get_rest('users').map { |user| user['user']['username'] }
303
- org_admins.delete_if { |user| !org_members.include?(user) || user == 'pivotal' }
304
- if org_admins[0] != nil
305
- # Using an org admin already on the destination server
306
- Chef::Config.node_name = org_admins[0]
307
- Chef::Config.client_key = webui_key
308
- else
309
- # No suitable org admins found, defaulting to pivotal
310
- ui.warn("No suitable Organizational Admins found. Defaulting to pivotal for org creation")
311
- end
312
- Chef::Config.custom_http_headers = (Chef::Config.custom_http_headers || {}).merge({'x-ops-request-source' => 'web'})
203
+ Chef::Config.node_name = org_admin
313
204
 
314
205
  # Restore the entire org skipping the admin data and restoring groups and acls last
315
- puts "Restoring the rest of the org"
206
+ ui.msg "Restoring the rest of the org"
207
+ ui.debug "Using admin user: #{org_admin}"
316
208
  chef_fs_config = Chef::ChefFS::Config.new
317
209
  top_level_paths = chef_fs_config.local_fs.children.select { |entry| entry.name != 'acls' && entry.name != 'groups' }.map { |entry| entry.path }
318
210
 
@@ -326,10 +218,9 @@ class Chef
326
218
  (top_level_paths + group_paths + group_acl_paths + acl_paths).each do |path|
327
219
  Chef::ChefFS::FileSystem.copy_to(Chef::ChefFS::FilePattern.new(path), chef_fs_config.local_fs, chef_fs_config.chef_fs, nil, config, ui, proc { |entry| chef_fs_config.format_path(entry) })
328
220
  end
329
- # restore clients to groups, using the pivotal key again
330
- Chef::Config[:node_name] = old_config[:node_name]
331
- Chef::Config[:client_key] = old_config[:client_key]
332
- Chef::Config.custom_http_headers = {}
221
+
222
+ # restore clients to groups, using the pivotal user again
223
+ Chef::Config[:node_name] = 'pivotal'
333
224
  ['admins', 'billing-admins'].each do |group|
334
225
  restore_group(Chef::ChefFS::Config.new, group)
335
226
  end
@@ -384,10 +275,6 @@ class Chef
384
275
  group.write(members.to_json)
385
276
  end
386
277
 
387
- def parallelize(entries, options = {}, &block)
388
- Chef::ChefFS::Parallelizer.parallelize(entries, options, &block)
389
- end
390
-
391
278
  def put_acl(rest, url, acls)
392
279
  old_acls = rest.get_rest(url)
393
280
  old_acls = Chef::ChefFS::DataHandler::AclDataHandler.new.normalize(old_acls, nil)