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

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