Class: Chef::Knife::EcRestore

Inherits:
Chef::Knife show all
Includes:
EcBase
Defined in:
lib/chef/knife/ec_restore.rb

Constant Summary collapse

PATHS =
%w(chef_repo_path cookbook_path environment_path data_bag_path role_path node_path client_path acl_path group_path container_path)
PERMISSIONS =
%w{create read update delete grant}.freeze

Instance Method Summary collapse

Methods included from EcBase

#completion_banner, #configure_chef, #ensure_webui_key_exists!, included, #knife_ec_error_handler, #local_user_list, #org_admin, #remote_user_list, #remote_users, #rest, #server, #set_client_config!, #set_dest_dir_from_args!, #set_skip_user_acl!, #temporary_webui_key, #user_acl_rest, #veil, #veil_config, #warn_on_incorrect_clients_group, #webui_key

Instance Method Details

#add_users_to_org(orgname) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/chef/knife/ec_restore.rb', line 97

def add_users_to_org(orgname)
  members = JSONCompat.from_json(File.read("#{dest_dir}/organizations/#{orgname}/members.json"))
  members.each do |member|
    username = member['user']['username']
    begin
      response = rest.post("organizations/#{orgname}/association_requests", { 'user' => username })
      association_id = response["uri"].split("/").last
      rest.put("users/#{username}/association_requests/#{association_id}", { 'response' => 'accept' })
    rescue Net::HTTPServerException => ex
      knife_ec_error_handler.add(ex) if ex.response.code != "409"
    end
  end
end

#chef_fs_copy_pattern(pattern_str, chef_fs_config) ⇒ Object

ChefFS copy pattern inside the EcRestore class will copy from the local_fs to the Chef Server.

NOTE: Do not get confused, this is the other way around from how we implemented in EcBackup. Therefor we can’t abstract it inside EcBase.



313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/chef/knife/ec_restore.rb', line 313

def chef_fs_copy_pattern(pattern_str, chef_fs_config)
  ui.msg "Copying #{pattern_str}"
  pattern = Chef::ChefFS::FilePattern.new(pattern_str)
  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) })
rescue Net::HTTPServerException,
       Chef::ChefFS::FileSystem::NotFoundError,
       Chef::ChefFS::FileSystem::OperationFailedError => ex
  knife_ec_error_handler.add(ex)
end

#create_organization(orgname) ⇒ Object



72
73
74
75
76
77
78
79
80
81
# File 'lib/chef/knife/ec_restore.rb', line 72

def create_organization(orgname)
  org = JSONCompat.from_json(File.read("#{dest_dir}/organizations/#{orgname}/org.json"))
  rest.post('organizations', org)
rescue Net::HTTPServerException => ex
  if ex.response.code == "409"
    rest.put("organizations/#{orgname}", org)
  else
    knife_ec_error_handler.add(ex)
  end
end

#ec_key_importObject



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/chef/knife/ec_restore.rb', line 180

def ec_key_import
  @ec_key_import ||= begin
                       require_relative 'ec_key_import'
                       k = Chef::Knife::EcKeyImport.new
                       k.name_args = ["#{dest_dir}/key_dump.json", "#{dest_dir}/key_table_dump.json"]
                       k.config[:skip_pivotal] = true
                       k.config[:skip_ids] = config[:skip_ids]
                       k.config[:sql_host] = config[:sql_host]
                       k.config[:sql_port] = config[:sql_port]
                       k.config[:sql_db] = config[:sql_db]
                       k.config[:sql_user] = config[:sql_user]
                       k.config[:sql_password] = config[:sql_password]
                       k
                     end
end

#for_each_organizationObject



131
132
133
134
135
136
137
# File 'lib/chef/knife/ec_restore.rb', line 131

def for_each_organization
  Dir.foreach("#{dest_dir}/organizations") do |name|
    next if name == '..' || name == '.' || !File.directory?("#{dest_dir}/organizations/#{name}")
    next unless (config[:org].nil? || config[:org] == name)
    yield name
  end
end

#for_each_userObject



119
120
121
122
123
124
125
126
127
128
129
# File 'lib/chef/knife/ec_restore.rb', line 119

def for_each_user
  Dir.foreach("#{dest_dir}/users") do |filename|
    next if filename !~ /(.+)\.json/
    name = $1
    if name == 'pivotal' && !config[:overwrite_pivotal]
      ui.warn("Skipping pivotal user.  To overwrite pivotal, pass --overwrite-pivotal.")
      next
    end
    yield name
  end
end

#group_array_to_sortable_hash(groups) ⇒ Object



332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/chef/knife/ec_restore.rb', line 332

def group_array_to_sortable_hash(groups)
  ret = {}
  groups.each do |group|
    name = group["name"]
    ret[name] = if group.key?("groups")
                  group["groups"]
                else
                  []
                end
  end
  ret
end

#purge_users_on_restoreObject



168
169
170
171
172
173
174
175
176
177
178
# File 'lib/chef/knife/ec_restore.rb', line 168

def purge_users_on_restore
  return unless config[:purge]
  users_for_purge do |user|
    ui.msg "Deleting user #{user} from remote (purge is on)"
    begin
      rest.delete("/users/#{user}")
    rescue Net::HTTPServerException => e
      ui.warn "Failed deleting user #{user} from remote #{e}"
    end
  end
end

#put_acl(rest, url, acls) ⇒ Object



377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/chef/knife/ec_restore.rb', line 377

def put_acl(rest, url, acls)
  old_acls = rest.get(url)
  old_acls = Chef::ChefFS::DataHandler::AclDataHandler.new.normalize(old_acls, nil)
  acls = Chef::ChefFS::DataHandler::AclDataHandler.new.normalize(acls, nil)
  if acls != old_acls
    PERMISSIONS.each do |permission|
      rest.put("#{url}/#{permission}", { permission => acls[permission] })
    end
  end
rescue Net::HTTPServerException => ex
  knife_ec_error_handler.add(ex)
end

#restore_group(chef_fs_config, group_name, includes = {:users => true, :clients => true}) ⇒ Object



345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# File 'lib/chef/knife/ec_restore.rb', line 345

def restore_group(chef_fs_config, group_name, includes = {:users => true, :clients => true})
  includes[:users] = true unless includes.key? :users
  includes[:clients] = true unless includes.key? :clients

  ui.msg "Copying /groups/#{group_name}.json"
  group = Chef::ChefFS::FileSystem.resolve_path(
    chef_fs_config.chef_fs,
    "/groups/#{group_name}.json"
  )

  # Will throw NotFoundError if JSON file does not exist on disk. See below.
  members_json = Chef::ChefFS::FileSystem.resolve_path(
    chef_fs_config.local_fs,
    "/groups/#{group_name}.json"
  ).read

  members = JSON.parse(members_json).select do |member|
    if includes[:users] and includes[:clients]
      member
    elsif includes[:users]
      member == 'users'
    elsif includes[:clients]
      member == 'clients'
    end
  end

  group.write(members.to_json)
rescue Chef::ChefFS::FileSystem::NotFoundError
  Chef::Log.warn "Could not find #{group.display_path} on disk. Will not restore."
end

#restore_key_sqlObject



205
206
207
208
209
210
211
212
# File 'lib/chef/knife/ec_restore.rb', line 205

def restore_key_sql
  k = ec_key_import
  k.config[:skip_users_table] = true
  k.config[:skip_keys_table] = false
  k.config[:users_only] = false
  k.config[:clients_only] = true
  k.run
end

#restore_open_invitations(orgname) ⇒ Object



83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/chef/knife/ec_restore.rb', line 83

def restore_open_invitations(orgname)
  invitations = JSONCompat.from_json(File.read("#{dest_dir}/organizations/#{orgname}/invitations.json"))
  invitations.each do |invitation|
    begin
      rest.post("organizations/#{orgname}/association_requests", { 'user' => invitation['username'] })
    rescue Net::HTTPServerException => ex
      if ex.response.code != "409"
        ui.error("Cannot create invitation #{invitation['id']}")
        knife_ec_error_handler.add(ex)
      end
    end
  end
end

#restore_user_aclsObject



111
112
113
114
115
116
117
# File 'lib/chef/knife/ec_restore.rb', line 111

def restore_user_acls
  ui.msg "Restoring user ACLs"
  for_each_user do |name|
    user_acl = JSONCompat.from_json(File.read("#{dest_dir}/user_acls/#{name}.json"))
    put_acl(user_acl_rest, "users/#{name}/_acl", user_acl)
  end
end

#restore_user_sqlObject



196
197
198
199
200
201
202
203
# File 'lib/chef/knife/ec_restore.rb', line 196

def restore_user_sql
  k = ec_key_import
  k.config[:knife_ec_error_handler] = knife_ec_error_handler
  k.config[:skip_users_table] = false
  k.config[:skip_keys_table] = !config[:with_key_sql]
  k.config[:users_only] = true
  k.run
end

#restore_usersObject



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/chef/knife/ec_restore.rb', line 139

def restore_users
  ui.msg "Restoring users"
  for_each_user do |name|
    user = JSONCompat.from_json(File.read("#{dest_dir}/users/#{name}.json"))
    begin
      # Supply password for new user
      user_with_password = user.dup
      user_with_password['password'] = SecureRandom.hex
      rest.post('users', user_with_password)
    rescue Net::HTTPServerException => ex
      if ex.response.code == "409"
        rest.put("users/#{name}", user)
        next
      end
      knife_ec_error_handler.add(ex)
    end
  end
  purge_users_on_restore
end

#runObject



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/chef/knife/ec_restore.rb', line 42

def run
  set_dest_dir_from_args!
  set_client_config!
  ensure_webui_key_exists!
  set_skip_user_acl!

  warn_on_incorrect_clients_group(dest_dir, "restore")

  restore_users unless config[:skip_users]
  restore_user_sql if config[:with_user_sql]

  for_each_organization do |orgname|
    ui.msg "Restoring organization[#{orgname}]"
    create_organization(orgname)
    restore_open_invitations(orgname)
    add_users_to_org(orgname)
    upload_org_data(orgname)
  end

  restore_key_sql if config[:with_key_sql]

  if config[:skip_useracl]
    ui.warn("Skipping user ACL update. To update user ACLs, remove --skip-useracl or upgrade your Enterprise Chef Server.")
  else
    restore_user_acls
  end

  completion_banner
end

#sort_groups_for_upload(groups) ⇒ Object

Takes an array of group objects and topologically sorts them



328
329
330
# File 'lib/chef/knife/ec_restore.rb', line 328

def sort_groups_for_upload(groups)
  Chef::Tsorter.new(group_array_to_sortable_hash(groups)).tsort
end

#upload_org_data(name) ⇒ Object



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/chef/knife/ec_restore.rb', line 215

def upload_org_data(name)
  old_config = Chef::Config.save

  begin
    # Clear out paths
    PATHS.each do |path|
      Chef::Config.delete(path.to_sym)
    end

    Chef::Config.chef_repo_path = "#{dest_dir}/organizations/#{name}"
    Chef::Config.versioned_cookbooks = true
    Chef::Config.chef_server_url = "#{server.root_url}/organizations/#{name}"

    # Upload the admins, public_key_read_access and billing-admins groups and acls
    ui.msg "Restoring org admin data"
    chef_fs_config = Chef::ChefFS::Config.new

    # Handle Admins, Billing Admins and Public Key Read Access seperately
    #
    # admins: We need to upload admins first so that we
    # can upload all of the other objects as a user in the org
    # rather than as pivotal.  Because the clients, and groups, don't
    # exist yet, we first upload the group with only the users.
    #
    # billing-admins: The default permissions on the
    # billing-admin group only give update permissions to
    # pivotal and members of the billing-admins group. Since we
    # can't unsure that the admin we choose for uploading will
    # be in the billing admins group, we have to upload this
    # group as pivotal.  Thus, we upload its users and ACL here,
    # and then update it again once all of the clients and
    # groups are uploaded.
    #
    # public_key_read_access: Similarly for public_key_read_access,
    # the default permissions only give read/update to
    # pivotal and members of the admins group. Use the same strategy
    # above here.
    #
    groups = ['admins', 'billing-admins']
    groups.push('public_key_read_access') if
      ::File.exist?(::File.join(chef_fs_config.local_fs.child_paths['groups'], 'public_key_read_access.json'))

    groups.each do |group|
      restore_group(chef_fs_config, group, :clients => false)
    end

    acls_groups_paths = ['/acls/groups/billing-admins.json']
    acls_groups_paths.push('/acls/groups/public_key_read_access.json') if
      ::File.exist?(::File.join(chef_fs_config.local_fs.child_paths['acls'], 'groups', 'public_key_read_access.json'))

    acls_groups_paths.each do |acl|
      chef_fs_copy_pattern(acl, chef_fs_config)
    end

    Chef::Config.node_name = if config[:skip_version]
                               org_admin
                             else
                               server.supports_defaulting_to_pivotal? ? 'pivotal' : org_admin
                             end

    # Restore the entire org skipping the admin data and restoring groups and acls last
    ui.msg "Restoring the rest of the org"
    chef_fs_config = Chef::ChefFS::Config.new
    top_level_paths = chef_fs_config.local_fs.children.select { |entry| entry.name != 'acls' && entry.name != 'groups' }.map { |entry| entry.path }

    # Topologically sort groups for upload
    filenames = ['billing-admins.json', 'public_key_read_access.json']
    unsorted_groups = Chef::ChefFS::FileSystem.list(chef_fs_config.local_fs, Chef::ChefFS::FilePattern.new('/groups/*')).select { |entry| ! filenames.include?(entry.name) }.map { |entry| JSON.parse(entry.read) }
    group_paths = sort_groups_for_upload(unsorted_groups).map { |group_name| "/groups/#{group_name}.json" }

    group_acl_paths = Chef::ChefFS::FileSystem.list(chef_fs_config.local_fs, Chef::ChefFS::FilePattern.new('/acls/groups/*')).select { |entry| ! filenames.include?(entry.name) }.map { |entry| entry.path }
    acl_paths = Chef::ChefFS::FileSystem.list(chef_fs_config.local_fs, Chef::ChefFS::FilePattern.new('/acls/*')).select { |entry| entry.name != 'groups' }.map { |entry| entry.path }

    # Store organization data in a particular order:
    # - clients must be uploaded before groups (in top_level_paths)
    # - groups must be uploaded before any acl's
    # - groups must be uploaded twice to account for Chef Server versions that don't
    #   accept group members on POST
    (top_level_paths + group_paths*2 + group_acl_paths + acl_paths).each do |path|
      chef_fs_copy_pattern(path, chef_fs_config)
    end

    # restore clients to groups, using the pivotal user again
    Chef::Config[:node_name] = 'pivotal'
    groups.each do |group|
      restore_group(Chef::ChefFS::Config.new, group)
    end
   ensure
    Chef::Config.restore(old_config)
  end
end

#users_for_purgeObject



159
160
161
162
163
164
165
166
# File 'lib/chef/knife/ec_restore.rb', line 159

def users_for_purge
  purge_list = remote_user_list - local_user_list
  # failsafe - don't delete pivotal
  purge_list -= ['pivotal']
  purge_list.each do |user|
    yield user
  end
end