diff options
author | John Keiser <john@johnkeiser.com> | 2016-01-07 14:18:44 -0800 |
---|---|---|
committer | John Keiser <john@johnkeiser.com> | 2016-01-13 09:23:07 -0800 |
commit | 6d9c8b03466632b3cd11042a2b99a08ae5047309 (patch) | |
tree | 0bbff68a54b99e7f6f70819783a75bc9967093d1 /lib/chef/chef_fs/file_system/chef_server | |
parent | 5e5cbe6df61ae69b9a5942ce51a7ad1f8bb6ea3a (diff) | |
download | chef-6d9c8b03466632b3cd11042a2b99a08ae5047309.tar.gz |
Move server and repository fs objects to their own directories
Diffstat (limited to 'lib/chef/chef_fs/file_system/chef_server')
22 files changed, 2088 insertions, 0 deletions
diff --git a/lib/chef/chef_fs/file_system/chef_server/acl_dir.rb b/lib/chef/chef_fs/file_system/chef_server/acl_dir.rb new file mode 100644 index 0000000000..fff47c986d --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/acl_dir.rb @@ -0,0 +1,65 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/base_fs_dir' +require 'chef/chef_fs/file_system/chef_server/acl_entry' +require 'chef/chef_fs/file_system/operation_not_allowed_error' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class AclDir < BaseFSDir + def api_path + parent.parent.child(name).api_path + end + + def make_child_entry(name, exists = nil) + result = @children.select { |child| child.name == name }.first if @children + result || AclEntry.new(name, self, exists) + end + + def can_have_child?(name, is_dir) + name =~ /\.json$/ && !is_dir + end + + def children + if @children.nil? + # Grab the ACTUAL children (/nodes, /containers, etc.) and get their names + names = parent.parent.child(name).children.map { |child| child.dir? ? "#{child.name}.json" : child.name } + @children = names.map { |name| make_child_entry(name, true) } + end + @children + end + + def create_child(name, file_contents) + raise OperationNotAllowedError.new(:create_child, self, nil, "ACLs can only be updated, and can only be created when the corresponding object is created.") + end + + def data_handler + parent.data_handler + end + + def rest + parent.rest + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/acl_entry.rb b/lib/chef/chef_fs/file_system/chef_server/acl_entry.rb new file mode 100644 index 0000000000..f87a7eaca9 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/acl_entry.rb @@ -0,0 +1,60 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/chef_server/rest_list_entry' +require 'chef/chef_fs/file_system/not_found_error' +require 'chef/chef_fs/file_system/operation_not_allowed_error' +require 'chef/chef_fs/file_system/operation_failed_error' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class AclEntry < RestListEntry + PERMISSIONS = %w(create read update delete grant) + + def api_path + "#{super}/_acl" + end + + def delete(recurse) + raise Chef::ChefFS::FileSystem::OperationNotAllowedError.new(:delete, self, nil, "ACLs cannot be deleted") + end + + def write(file_contents) + # ACL writes are fun. + acls = data_handler.normalize(Chef::JSONCompat.parse(file_contents), self) + PERMISSIONS.each do |permission| + begin + rest.put("#{api_path}/#{permission}", { permission => acls[permission] }) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "Timeout writing: #{e}") + rescue Net::HTTPServerException => e + if e.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "HTTP error writing: #{e}") + end + end + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/acls_dir.rb b/lib/chef/chef_fs/file_system/chef_server/acls_dir.rb new file mode 100644 index 0000000000..bc7168e31d --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/acls_dir.rb @@ -0,0 +1,70 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/base_fs_dir' +require 'chef/chef_fs/file_system/chef_server/acl_dir' +require 'chef/chef_fs/file_system/chef_server/cookbooks_acl_dir' +require 'chef/chef_fs/file_system/chef_server/acl_entry' +require 'chef/chef_fs/data_handler/acl_data_handler' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class AclsDir < BaseFSDir + ENTITY_TYPES = %w(clients containers cookbooks data_bags environments groups nodes roles) # we don't read sandboxes, so we don't read their acls + + def data_handler + @data_handler ||= Chef::ChefFS::DataHandler::AclDataHandler.new + end + + def api_path + parent.api_path + end + + def make_child_entry(name) + children.select { |child| child.name == name }.first + end + + def can_have_child?(name, is_dir) + is_dir ? ENTITY_TYPES.include?(name) : name == 'organization.json' + end + + def children + if @children.nil? + @children = ENTITY_TYPES.map do |entity_type| + case entity_type + when 'cookbooks' + CookbooksAclDir.new(entity_type, self) + else + AclDir.new(entity_type, self) + end + end + @children << AclEntry.new('organization.json', self, true) # the org acl is retrieved as GET /organizations/ORGNAME/ANYTHINGATALL/_acl + end + @children + end + + def rest + parent.rest + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/chef_server_root_dir.rb b/lib/chef/chef_fs/file_system/chef_server/chef_server_root_dir.rb new file mode 100644 index 0000000000..1ac03166c2 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/chef_server_root_dir.rb @@ -0,0 +1,192 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/server_api' +require 'chef/chef_fs/file_system/chef_server/acls_dir' +require 'chef/chef_fs/file_system/base_fs_dir' +require 'chef/chef_fs/file_system/chef_server/rest_list_dir' +require 'chef/chef_fs/file_system/chef_server/cookbooks_dir' +require 'chef/chef_fs/file_system/chef_server/data_bags_dir' +require 'chef/chef_fs/file_system/chef_server/nodes_dir' +require 'chef/chef_fs/file_system/chef_server/org_entry' +require 'chef/chef_fs/file_system/chef_server/organization_invites_entry' +require 'chef/chef_fs/file_system/chef_server/organization_members_entry' +require 'chef/chef_fs/file_system/chef_server/policies_dir' +require 'chef/chef_fs/file_system/chef_server/policy_groups_dir' +require 'chef/chef_fs/file_system/chef_server/environments_dir' +require 'chef/chef_fs/data_handler/acl_data_handler' +require 'chef/chef_fs/data_handler/client_data_handler' +require 'chef/chef_fs/data_handler/environment_data_handler' +require 'chef/chef_fs/data_handler/node_data_handler' +require 'chef/chef_fs/data_handler/role_data_handler' +require 'chef/chef_fs/data_handler/user_data_handler' +require 'chef/chef_fs/data_handler/group_data_handler' +require 'chef/chef_fs/data_handler/container_data_handler' +require 'chef/chef_fs/data_handler/policy_group_data_handler' + +class Chef + module ChefFS + module FileSystem + module ChefServer + # + # Represents the root of a Chef server (or organization), under which + # nodes, roles, cookbooks, etc. can be found. + # + class ChefServerRootDir < BaseFSDir + # + # Create a new Chef server root. + # + # == Parameters + # + # [root_name] + # A friendly name for the root, for printing--like "remote" or "chef_central". + # [chef_config] + # A hash with options that look suspiciously like Chef::Config, including the + # following keys: + # :chef_server_url:: The URL to the Chef server or top of the organization + # :node_name:: The username to authenticate to the Chef server with + # :client_key:: The private key for the user for authentication + # :environment:: The environment in which you are presently working + # :repo_mode:: + # The repository mode, :hosted_everything, :everything or :static. + # This determines the set of subdirectories the Chef server will + # offer up. + # :versioned_cookbooks:: whether or not to include versions in cookbook names + # [options] + # Other options: + # :cookbook_version:: when cookbooks are retrieved, grab this version for them. + # :freeze:: freeze cookbooks on upload + # + def initialize(root_name, chef_config, options = {}) + super("", nil) + @chef_server_url = chef_config[:chef_server_url] + @chef_username = chef_config[:node_name] + @chef_private_key = chef_config[:client_key] + @environment = chef_config[:environment] + @repo_mode = chef_config[:repo_mode] + @versioned_cookbooks = chef_config[:versioned_cookbooks] + @root_name = root_name + @cookbook_version = options[:cookbook_version] # Used in knife diff and download for server cookbook version + end + + attr_reader :chef_server_url + attr_reader :chef_username + attr_reader :chef_private_key + attr_reader :environment + attr_reader :repo_mode + attr_reader :cookbook_version + attr_reader :versioned_cookbooks + + def fs_description + "Chef server at #{chef_server_url} (user #{chef_username}), repo_mode = #{repo_mode}" + end + + def rest + Chef::ServerAPI.new(chef_server_url, :client_name => chef_username, :signing_key_filename => chef_private_key, :raw_output => true, :api_version => "0") + end + + def get_json(path) + Chef::ServerAPI.new(chef_server_url, :client_name => chef_username, :signing_key_filename => chef_private_key, :api_version => "0").get(path) + end + + def chef_rest + Chef::ServerAPI.new(chef_server_url, :client_name => chef_username, :signing_key_filename => chef_private_key) + end + + def api_path + "" + end + + def path_for_printing + "#{@root_name}/" + end + + def can_have_child?(name, is_dir) + result = children.select { |child| child.name == name }.first + result && !!result.dir? == !!is_dir + end + + def org + @org ||= begin + path = Pathname.new(URI.parse(chef_server_url).path).cleanpath + if File.dirname(path) == '/organizations' + File.basename(path) + else + # In Chef 12, everything is in an org. + 'chef' + end + end + end + + def make_child_entry(name) + children.select { |child| child.name == name }.first + end + + def children + @children ||= begin + result = [ + # /cookbooks + CookbooksDir.new("cookbooks", self), + # /data_bags + DataBagsDir.new("data_bags", self, "data"), + # /environments + EnvironmentsDir.new("environments", self, nil, Chef::ChefFS::DataHandler::EnvironmentDataHandler.new), + # /roles + RestListDir.new("roles", self, nil, Chef::ChefFS::DataHandler::RoleDataHandler.new) + ] + if repo_mode == 'hosted_everything' + result += [ + # /acls + AclsDir.new("acls", self), + # /clients + RestListDir.new("clients", self, nil, Chef::ChefFS::DataHandler::ClientDataHandler.new), + # /containers + RestListDir.new("containers", self, nil, Chef::ChefFS::DataHandler::ContainerDataHandler.new), + # /groups + RestListDir.new("groups", self, nil, Chef::ChefFS::DataHandler::GroupDataHandler.new), + # /nodes + NodesDir.new("nodes", self, nil, Chef::ChefFS::DataHandler::NodeDataHandler.new), + # /org.json + OrgEntry.new("org.json", self), + # /members.json + OrganizationMembersEntry.new("members.json", self), + # /invitations.json + OrganizationInvitesEntry.new("invitations.json", self), + # /policies + PoliciesDir.new("policies", self, nil, Chef::ChefFS::DataHandler::PolicyDataHandler.new), + # /policy_groups + PolicyGroupsDir.new("policy_groups", self, nil, Chef::ChefFS::DataHandler::PolicyGroupDataHandler.new), + ] + elsif repo_mode != 'static' + result += [ + # /clients + RestListDir.new("clients", self, nil, Chef::ChefFS::DataHandler::ClientDataHandler.new), + # /nodes + NodesDir.new("nodes", self, nil, Chef::ChefFS::DataHandler::NodeDataHandler.new), + # /users + RestListDir.new("users", self, nil, Chef::ChefFS::DataHandler::UserDataHandler.new) + ] + end + result.sort_by { |child| child.name } + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/cookbook_dir.rb b/lib/chef/chef_fs/file_system/chef_server/cookbook_dir.rb new file mode 100644 index 0000000000..51de0bfd86 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/cookbook_dir.rb @@ -0,0 +1,226 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/command_line' +require 'chef/chef_fs/file_system/chef_server/rest_list_dir' +require 'chef/chef_fs/file_system/chef_server/cookbook_subdir' +require 'chef/chef_fs/file_system/chef_server/cookbook_file' +require 'chef/chef_fs/file_system/not_found_error' +require 'chef/cookbook_version' +require 'chef/cookbook_uploader' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class CookbookDir < BaseFSDir + def initialize(name, parent, options = {}) + super(name, parent) + @exists = options[:exists] + # If the name is apache2-1.0.0 and versioned_cookbooks is on, we know + # the actual cookbook_name and version. + if root.versioned_cookbooks + if name =~ VALID_VERSIONED_COOKBOOK_NAME + @cookbook_name = $1 + @version = $2 + else + @exists = false + end + else + @cookbook_name = name + @version = root.cookbook_version # nil unless --cookbook-version specified in download/diff + end + end + + attr_reader :cookbook_name, :version + + COOKBOOK_SEGMENT_INFO = { + :attributes => { :ruby_only => true }, + :definitions => { :ruby_only => true }, + :recipes => { :ruby_only => true }, + :libraries => { :ruby_only => true }, + :templates => { :recursive => true }, + :files => { :recursive => true }, + :resources => { :ruby_only => true, :recursive => true }, + :providers => { :ruby_only => true, :recursive => true }, + :root_files => { } + } + + # See Erchef code + # https://github.com/opscode/chef_objects/blob/968a63344d38fd507f6ace05f73d53e9cd7fb043/src/chef_regex.erl#L94 + VALID_VERSIONED_COOKBOOK_NAME = /^([.a-zA-Z0-9_-]+)-(\d+\.\d+\.\d+)$/ + + def add_child(child) + @children << child + end + + def api_path + "#{parent.api_path}/#{cookbook_name}/#{version || "_latest"}" + end + + def make_child_entry(name) + # Since we're ignoring the rules and doing a network request here, + # we need to make sure we don't rethrow the exception. (child(name) + # is not supposed to fail.) + begin + children.select { |child| child.name == name }.first + rescue Chef::ChefFS::FileSystem::NotFoundError + nil + end + end + + def can_have_child?(name, is_dir) + # A cookbook's root may not have directories unless they are segment directories + return name != 'root_files' && COOKBOOK_SEGMENT_INFO.keys.include?(name.to_sym) if is_dir + return true + end + + def children + if @children.nil? + @children = [] + manifest = chef_object.manifest + COOKBOOK_SEGMENT_INFO.each do |segment, segment_info| + next unless manifest.has_key?(segment) + + # Go through each file in the manifest for the segment, and + # add cookbook subdirs and files for it. + manifest[segment].each do |segment_file| + parts = segment_file[:path].split('/') + # Get or create the path to the file + container = self + parts[0,parts.length-1].each do |part| + old_container = container + container = old_container.children.select { |child| part == child.name }.first + if !container + container = CookbookSubdir.new(part, old_container, segment_info[:ruby_only], segment_info[:recursive]) + old_container.add_child(container) + end + end + # Create the file itself + container.add_child(CookbookFile.new(parts[parts.length-1], container, segment_file)) + end + end + @children = @children.sort_by { |c| c.name } + end + @children + end + + def dir? + exists? + end + + def delete(recurse) + if recurse + begin + rest.delete(api_path) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e, "Timeout deleting: #{e}") + rescue Net::HTTPServerException + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e, "HTTP error deleting: #{e}") + end + end + else + raise NotFoundError.new(self) if !exists? + raise MustDeleteRecursivelyError.new(self, "#{path_for_printing} must be deleted recursively") + end + end + + # In versioned cookbook mode, actually check if the version exists + # Probably want to cache this. + def exists? + if @exists.nil? + @exists = parent.children.any? { |child| child.name == name } + end + @exists + end + + def compare_to(other) + if !other.dir? + return [ !exists?, nil, nil ] + end + are_same = true + Chef::ChefFS::CommandLine::diff_entries(self, other, nil, :name_only).each do |type, old_entry, new_entry| + if [ :directory_to_file, :file_to_directory, :deleted, :added, :modified ].include?(type) + are_same = false + end + end + [ are_same, nil, nil ] + end + + def copy_from(other, options = {}) + parent.upload_cookbook_from(other, options) + end + + def rest + parent.rest + end + + def chef_object + # We cheat and cache here, because it seems like a good idea to keep + # the cookbook view consistent with the directory structure. + return @chef_object if @chef_object + + # The negative (not found) response is cached + if @could_not_get_chef_object + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, @could_not_get_chef_object) + end + + begin + # We want to fail fast, for now, because of the 500 issue :/ + # This will make things worse for parallelism, a little, because + # Chef::Config is global and this could affect other requests while + # this request is going on. (We're not parallel yet, but we will be.) + # Chef bug http://tickets.opscode.com/browse/CHEF-3066 + old_retry_count = Chef::Config[:http_retry_count] + begin + Chef::Config[:http_retry_count] = 0 + @chef_object ||= Chef::CookbookVersion.json_create(root.get_json(api_path)) + ensure + Chef::Config[:http_retry_count] = old_retry_count + end + + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "Timeout reading: #{e}") + + rescue Net::HTTPServerException => e + if e.response.code == "404" + @could_not_get_chef_object = e + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, @could_not_get_chef_object) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "HTTP error reading: #{e}") + end + + # Chef bug http://tickets.opscode.com/browse/CHEF-3066 ... instead of 404 we get 500 right now. + # Remove this when that bug is fixed. + rescue Net::HTTPFatalError => e + if e.response.code == "500" + @could_not_get_chef_object = e + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, @could_not_get_chef_object) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "HTTP error reading: #{e}") + end + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/cookbook_file.rb b/lib/chef/chef_fs/file_system/chef_server/cookbook_file.rb new file mode 100644 index 0000000000..721c5092cf --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/cookbook_file.rb @@ -0,0 +1,84 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/base_fs_object' +require 'chef/http/simple' +require 'openssl' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class CookbookFile < BaseFSObject + def initialize(name, parent, file) + super(name, parent) + @file = file + end + + attr_reader :file + + def checksum + file[:checksum] + end + + def read + begin + tmpfile = rest.streaming_request(file[:url]) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "Timeout reading #{file[:url]}: #{e}") + rescue Net::HTTPServerException => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "#{e.message} retrieving #{file[:url]}") + end + + begin + tmpfile.open + tmpfile.read + ensure + tmpfile.close! + end + end + + def rest + parent.rest + end + + def compare_to(other) + other_value = nil + if other.respond_to?(:checksum) + other_checksum = other.checksum + else + begin + other_value = other.read + rescue Chef::ChefFS::FileSystem::NotFoundError + return [ false, nil, :none ] + end + other_checksum = calc_checksum(other_value) + end + [ checksum == other_checksum, nil, other_value ] + end + + private + + def calc_checksum(value) + OpenSSL::Digest::MD5.hexdigest(value) + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/cookbook_subdir.rb b/lib/chef/chef_fs/file_system/chef_server/cookbook_subdir.rb new file mode 100644 index 0000000000..55c0168077 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/cookbook_subdir.rb @@ -0,0 +1,61 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/base_fs_dir' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class CookbookSubdir < BaseFSDir + def initialize(name, parent, ruby_only, recursive) + super(name, parent) + @children = [] + @ruby_only = ruby_only + @recursive = recursive + end + + attr_reader :versions + attr_reader :children + + def add_child(child) + @children << child + end + + def can_have_child?(name, is_dir) + if is_dir + return false if !@recursive + else + return false if @ruby_only && name !~ /\.rb$/ + end + true + end + + def make_child_entry(name) + result = @children.select { |child| child.name == name }.first if @children + result || NonexistentFSObject.new(name, self) + end + + def rest + parent.rest + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/cookbooks_acl_dir.rb b/lib/chef/chef_fs/file_system/chef_server/cookbooks_acl_dir.rb new file mode 100644 index 0000000000..999fb58f79 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/cookbooks_acl_dir.rb @@ -0,0 +1,43 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/chef_server/acl_dir' +require 'chef/chef_fs/file_system/chef_server/acl_entry' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class CookbooksAclDir < AclDir + # If versioned_cookbooks is on, the list of cookbooks will have versions + # in them. But all versions of a cookbook have the same acl, so even if + # we have cookbooks/apache2-1.0.0 and cookbooks/apache2-1.1.2, we will + # only have one acl: acls/cookbooks/apache2.json. Thus, the list of + # children of acls/cookbooks is a unique list of cookbook *names*. + def children + if @children.nil? + names = parent.parent.child(name).children.map { |child| "#{child.cookbook_name}.json" } + @children = names.uniq.map { |name| make_child_entry(name, true) } + end + @children + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/cookbooks_dir.rb b/lib/chef/chef_fs/file_system/chef_server/cookbooks_dir.rb new file mode 100644 index 0000000000..28d88ea330 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/cookbooks_dir.rb @@ -0,0 +1,154 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/chef_server/rest_list_dir' +require 'chef/chef_fs/file_system/chef_server/cookbook_dir' +require 'chef/chef_fs/file_system/operation_failed_error' +require 'chef/chef_fs/file_system/cookbook_frozen_error' +require 'chef/chef_fs/file_system/repository/chef_repository_file_system_cookbook_dir' +require 'chef/mixin/file_class' + +require 'tmpdir' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class CookbooksDir < RestListDir + + include Chef::Mixin::FileClass + + def make_child_entry(name) + result = @children.select { |child| child.name == name }.first if @children + result || CookbookDir.new(name, self) + end + + def children + @children ||= begin + if root.versioned_cookbooks + result = [] + root.get_json("#{api_path}/?num_versions=all").each_pair do |cookbook_name, cookbooks| + cookbooks['versions'].each do |cookbook_version| + result << CookbookDir.new("#{cookbook_name}-#{cookbook_version['version']}", self, :exists => true) + end + end + else + result = root.get_json(api_path).keys.map { |cookbook_name| CookbookDir.new(cookbook_name, self, :exists => true) } + end + result.sort_by(&:name) + end + end + + def create_child_from(other, options = {}) + @children = nil + upload_cookbook_from(other, options) + end + + def upload_cookbook_from(other, options = {}) + root.versioned_cookbooks ? upload_versioned_cookbook(other, options) : upload_unversioned_cookbook(other, options) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "Timeout writing: #{e}") + rescue Net::HTTPServerException => e + case e.response.code + when "409" + raise Chef::ChefFS::FileSystem::CookbookFrozenError.new(:write, self, e, "Cookbook #{other.name} is frozen") + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "HTTP error writing: #{e}") + end + rescue Chef::Exceptions::CookbookFrozen => e + raise Chef::ChefFS::FileSystem::CookbookFrozenError.new(:write, self, e, "Cookbook #{other.name} is frozen") + end + + # Knife currently does not understand versioned cookbooks + # Cookbook Version uploader also requires a lot of refactoring + # to make this work. So instead, we make a temporary cookbook + # symlinking back to real cookbook, and upload the proxy. + def upload_versioned_cookbook(other, options) + cookbook_name = Chef::ChefFS::FileSystem::Repository::ChefRepositoryFileSystemCookbookDir.canonical_cookbook_name(other.name) + + Dir.mktmpdir do |temp_cookbooks_path| + proxy_cookbook_path = "#{temp_cookbooks_path}/#{cookbook_name}" + + # Make a symlink + file_class.symlink other.file_path, proxy_cookbook_path + + # Instantiate a proxy loader using the temporary symlink + proxy_loader = Chef::Cookbook::CookbookVersionLoader.new(proxy_cookbook_path, other.parent.chefignore) + proxy_loader.load_cookbooks + + cookbook_to_upload = proxy_loader.cookbook_version + cookbook_to_upload.freeze_version if options[:freeze] + + # Instantiate a new uploader based on the proxy loader + uploader = Chef::CookbookUploader.new(cookbook_to_upload, :force => options[:force], :rest => root.chef_rest) + + with_actual_cookbooks_dir(temp_cookbooks_path) do + upload_cookbook!(uploader) + end + + # + # When the temporary directory is being deleted on + # windows, the contents of the symlink under that + # directory is also deleted. So explicitly remove + # the symlink without removing the original contents if we + # are running on windows + # + if Chef::Platform.windows? + Dir.rmdir proxy_cookbook_path + end + end + end + + def upload_unversioned_cookbook(other, options) + cookbook_to_upload = other.chef_object + cookbook_to_upload.freeze_version if options[:freeze] + uploader = Chef::CookbookUploader.new(cookbook_to_upload, :force => options[:force], :rest => root.chef_rest) + + with_actual_cookbooks_dir(other.parent.file_path) do + upload_cookbook!(uploader) + end + end + + # Work around the fact that CookbookUploader doesn't understand chef_repo_path (yet) + def with_actual_cookbooks_dir(actual_cookbook_path) + old_cookbook_path = Chef::Config.cookbook_path + Chef::Config.cookbook_path = actual_cookbook_path if !Chef::Config.cookbook_path + + yield + ensure + Chef::Config.cookbook_path = old_cookbook_path + end + + def upload_cookbook!(uploader, options = {}) + if uploader.respond_to?(:upload_cookbook) + uploader.upload_cookbook + else + uploader.upload_cookbooks + end + end + + def can_have_child?(name, is_dir) + return false if !is_dir + return false if root.versioned_cookbooks && name !~ Chef::ChefFS::FileSystem::ChefServer::CookbookDir::VALID_VERSIONED_COOKBOOK_NAME + return true + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/data_bag_dir.rb b/lib/chef/chef_fs/file_system/chef_server/data_bag_dir.rb new file mode 100644 index 0000000000..b657243d64 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/data_bag_dir.rb @@ -0,0 +1,71 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/chef_server/rest_list_dir' +require 'chef/chef_fs/file_system/not_found_error' +require 'chef/chef_fs/file_system/must_delete_recursively_error' +require 'chef/chef_fs/data_handler/data_bag_item_data_handler' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class DataBagDir < RestListDir + def initialize(name, parent, exists = nil) + super(name, parent, nil, Chef::ChefFS::DataHandler::DataBagItemDataHandler.new) + @exists = nil + end + + def dir? + exists? + end + + def read + # This will only be called if dir? is false, which means exists? is false. + raise Chef::ChefFS::FileSystem::NotFoundError.new(self) + end + + def exists? + if @exists.nil? + @exists = parent.children.any? { |child| child.name == name } + end + @exists + end + + def delete(recurse) + if !recurse + raise NotFoundError.new(self) if !exists? + raise MustDeleteRecursivelyError.new(self, "#{path_for_printing} must be deleted recursively") + end + begin + rest.delete(api_path) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e, "Timeout deleting: #{e}") + rescue Net::HTTPServerException => e + if e.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e, "HTTP error deleting: #{e}") + end + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/data_bags_dir.rb b/lib/chef/chef_fs/file_system/chef_server/data_bags_dir.rb new file mode 100644 index 0000000000..50952cfc1b --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/data_bags_dir.rb @@ -0,0 +1,69 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/chef_server/rest_list_dir' +require 'chef/chef_fs/file_system/chef_server/data_bag_dir' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class DataBagsDir < RestListDir + def make_child_entry(name, exists = false) + result = @children.select { |child| child.name == name }.first if @children + result || DataBagDir.new(name, self, exists) + end + + def children + begin + @children ||= root.get_json(api_path).keys.sort.map { |entry| make_child_entry(entry, true) } + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "Timeout getting children: #{e}") + rescue Net::HTTPServerException => e + if e.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "HTTP error getting children: #{e}") + end + end + end + + def can_have_child?(name, is_dir) + is_dir + end + + def create_child(name, file_contents) + begin + rest.post(api_path, { 'name' => name }) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Timeout creating child '#{name}': #{e}") + rescue Net::HTTPServerException => e + if e.response.code == "409" + raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, self, e, "Cannot create #{name} under #{path}: already exists") + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "HTTP error creating child '#{name}': #{e}") + end + end + @children = nil + DataBagDir.new(name, self, true) + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/environments_dir.rb b/lib/chef/chef_fs/file_system/chef_server/environments_dir.rb new file mode 100644 index 0000000000..721c8a38d4 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/environments_dir.rb @@ -0,0 +1,57 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/base_fs_dir' +require 'chef/chef_fs/file_system/chef_server/rest_list_entry' +require 'chef/chef_fs/file_system/not_found_error' +require 'chef/chef_fs/file_system/default_environment_cannot_be_modified_error' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class EnvironmentsDir < RestListDir + def make_child_entry(name, exists = nil) + if name == '_default.json' + DefaultEnvironmentEntry.new(name, self, exists) + else + super + end + end + + class DefaultEnvironmentEntry < RestListEntry + def initialize(name, parent, exists = nil) + super(name, parent) + @exists = exists + end + + def delete(recurse) + raise NotFoundError.new(self) if !exists? + raise DefaultEnvironmentCannotBeModifiedError.new(:delete, self) + end + + def write(file_contents) + raise NotFoundError.new(self) if !exists? + raise DefaultEnvironmentCannotBeModifiedError.new(:write, self) + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/nodes_dir.rb b/lib/chef/chef_fs/file_system/chef_server/nodes_dir.rb new file mode 100644 index 0000000000..c0728fbe1f --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/nodes_dir.rb @@ -0,0 +1,53 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/base_fs_dir' +require 'chef/chef_fs/file_system/chef_server/rest_list_entry' +require 'chef/chef_fs/file_system/not_found_error' +require 'chef/chef_fs/data_handler/node_data_handler' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class NodesDir < RestListDir + # Identical to RestListDir.children, except supports environments + def children + begin + @children ||= root.get_json(env_api_path).keys.sort.map do |key| + make_child_entry("#{key}.json", true) + end + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "Timeout retrieving children: #{e}") + rescue Net::HTTPServerException => e + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "HTTP error retrieving children: #{e}") + end + end + end + + def env_api_path + environment ? "environments/#{environment}/#{api_path}" : api_path + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/org_entry.rb b/lib/chef/chef_fs/file_system/chef_server/org_entry.rb new file mode 100644 index 0000000000..82c5764b42 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/org_entry.rb @@ -0,0 +1,31 @@ +require 'chef/chef_fs/file_system/chef_server/rest_list_entry' +require 'chef/chef_fs/data_handler/organization_data_handler' + +class Chef + module ChefFS + module FileSystem + module ChefServer + # /organizations/NAME/org.json + # Represents the actual data at /organizations/NAME (the full name, etc.) + class OrgEntry < RestListEntry + def data_handler + Chef::ChefFS::DataHandler::OrganizationDataHandler.new + end + + # /organizations/foo/org.json -> GET /organizations/foo + def api_path + parent.api_path + end + + def exists? + parent.exists? + end + + def delete(recurse) + raise Chef::ChefFS::FileSystem::OperationNotAllowedError.new(:delete, self) + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/organization_invites_entry.rb b/lib/chef/chef_fs/file_system/chef_server/organization_invites_entry.rb new file mode 100644 index 0000000000..cf276c6466 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/organization_invites_entry.rb @@ -0,0 +1,61 @@ +require 'chef/chef_fs/file_system/chef_server/rest_list_entry' +require 'chef/chef_fs/data_handler/organization_invites_data_handler' +require 'chef/json_compat' + +class Chef + module ChefFS + module FileSystem + module ChefServer + # /organizations/NAME/invitations.json + # read data from: + # - GET /organizations/NAME/association_requests + # write data to: + # - remove from list: DELETE /organizations/NAME/association_requests/id + # - add to list: POST /organizations/NAME/association_requests + class OrganizationInvitesEntry < RestListEntry + def initialize(name, parent, exists = nil) + super(name, parent) + @exists = exists + end + + def data_handler + Chef::ChefFS::DataHandler::OrganizationInvitesDataHandler.new + end + + # /organizations/foo/invites.json -> /organizations/foo/association_requests + def api_path + File.join(parent.api_path, 'association_requests') + end + + def exists? + parent.exists? + end + + def delete(recurse) + raise Chef::ChefFS::FileSystem::OperationNotAllowedError.new(:delete, self) + end + + def write(contents) + desired_invites = minimize_value(Chef::JSONCompat.parse(contents, :create_additions => false)) + actual_invites = _read_json.inject({}) { |h,val| h[val['username']] = val['id']; h } + invites = actual_invites.keys + (desired_invites - invites).each do |invite| + begin + rest.post(api_path, { 'user' => invite }) + rescue Net::HTTPServerException => e + if e.response.code == '409' + Chef::Log.warn("Could not invite #{invite} to organization #{org}: #{api_error_text(e.response)}") + else + raise + end + end + end + (invites - desired_invites).each do |invite| + rest.delete(File.join(api_path, actual_invites[invite])) + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/organization_members_entry.rb b/lib/chef/chef_fs/file_system/chef_server/organization_members_entry.rb new file mode 100644 index 0000000000..1c1c231643 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/organization_members_entry.rb @@ -0,0 +1,60 @@ +require 'chef/chef_fs/file_system/chef_server/rest_list_entry' +require 'chef/chef_fs/data_handler/organization_members_data_handler' +require 'chef/json_compat' + +class Chef + module ChefFS + module FileSystem + module ChefServer + # /organizations/NAME/members.json + # reads data from: + # - GET /organizations/NAME/users + # writes data to: + # - remove from list: DELETE /organizations/NAME/users/name + # - add to list: POST /organizations/NAME/users/name + class OrganizationMembersEntry < RestListEntry + def initialize(name, parent, exists = nil) + super(name, parent) + @exists = exists + end + + def data_handler + Chef::ChefFS::DataHandler::OrganizationMembersDataHandler.new + end + + # /organizations/foo/members.json -> /organizations/foo/users + def api_path + File.join(parent.api_path, 'users') + end + + def exists? + parent.exists? + end + + def delete(recurse) + raise Chef::ChefFS::FileSystem::OperationNotAllowedError.new(:delete, self) + end + + def write(contents) + desired_members = minimize_value(Chef::JSONCompat.parse(contents, :create_additions => false)) + members = minimize_value(_read_json) + (desired_members - members).each do |member| + begin + rest.post(api_path, 'username' => member) + rescue Net::HTTPServerException => e + if %w(404 405).include?(e.response.code) + raise "Chef server at #{api_path} does not allow you to directly add members. Please either upgrade your Chef server or move the users you want into invitations.json instead of members.json." + else + raise + end + end + end + (members - desired_members).each do |member| + rest.delete(File.join(api_path, member)) + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/policies_dir.rb b/lib/chef/chef_fs/file_system/chef_server/policies_dir.rb new file mode 100644 index 0000000000..b6c34cfee7 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/policies_dir.rb @@ -0,0 +1,160 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/chef_server/rest_list_dir' +require 'chef/chef_fs/file_system/chef_server/policy_revision_entry' + +class Chef + module ChefFS + module FileSystem + module ChefServer + # + # Server API: + # /policies - list of policies by name + # - /policies/NAME - represents a policy with all revisions + # - /policies/NAME/revisions - list of revisions for that policy + # - /policies/NAME/revisions/REVISION - actual policy-revision document + # + # Local Repository and ChefFS: + # /policies - PoliciesDir - maps to server API /policies + # - /policies/NAME-REVISION.json - PolicyRevision - maps to /policies/NAME/revisions/REVISION + # + class PoliciesDir < RestListDir + # Children: NAME-REVISION.json for all revisions of all policies + # + # /nodes: { + # "node1": "https://api.opscode.com/organizations/myorg/nodes/node1", + # "node2": "https://api.opscode.com/organizations/myorg/nodes/node2", + # } + # + # /policies: { + # "foo": {} + # } + + def make_child_entry(name, exists = nil) + @children.select { |child| child.name == name }.first if @children + PolicyRevisionEntry.new(name, self, exists) + end + + # Children come from /policies in this format: + # { + # "foo": { + # "uri": "https://api.opscode.com/organizations/essentials/policies/foo", + # "revisions": { + # "1.0.0": { + # + # }, + # "1.0.1": { + # + # } + # } + # } + # } + def children + begin + # Grab the names of the children, append json, and make child entries + @children ||= begin + result = [] + data = root.get_json(api_path) + data.keys.sort.each do |policy_name| + data[policy_name]["revisions"].keys.each do |policy_revision| + filename = "#{policy_name}-#{policy_revision}.json" + result << make_child_entry(filename, true) + end + end + result + end + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "Timeout retrieving children: #{e}") + rescue Net::HTTPServerException => e + # 404 = NotFoundError + if $!.response.code == "404" + # GET /organizations/ORG/policies returned 404, but that just might be because + # we are talking to an older version of the server that doesn't support policies. + # Do GET /orgqanizations/ORG to find out if the org exists at all. + # TODO use server API version instead of a second network request. + begin + root.get_json(parent.api_path) + # Return empty list if the organization exists but /policies didn't work + [] + rescue Net::HTTPServerException => e + if e.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) + end + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "HTTP error retrieving children: #{e}") + end + # Anything else is unexpected (OperationFailedError) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "HTTP error retrieving children: #{e}") + end + end + end + + # + # Does POST <api_path> with file_contents + # + def create_child(name, file_contents) + # Parse the contents to ensure they are valid JSON + begin + object = Chef::JSONCompat.parse(file_contents) + rescue Chef::Exceptions::JSON::ParseError => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Parse error reading JSON creating child '#{name}': #{e}") + end + + # Create the child entry that will be returned + result = make_child_entry(name, true) + + # Normalize the file_contents before post (add defaults, etc.) + if data_handler + object = data_handler.normalize_for_post(object, result) + data_handler.verify_integrity(object, result) do |error| + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, nil, "Error creating '#{name}': #{error}") + end + end + + # POST /api_path with the normalized file_contents + begin + policy_name, policy_revision = data_handler.name_and_revision(name) + rest.post("#{api_path}/#{policy_name}/revisions", object) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Timeout creating '#{name}': #{e}") + rescue Net::HTTPServerException => e + # 404 = NotFoundError + if e.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) + # 409 = AlreadyExistsError + elsif $!.response.code == "409" + raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, self, e, "Failure creating '#{name}': #{path}/#{name} already exists") + # Anything else is unexpected (OperationFailedError) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Failure creating '#{name}': #{e.message}") + end + end + + # Clear the cache of children so that if someone asks for children + # again, we will get it again + @children = nil + + result + end + + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/policy_group_entry.rb b/lib/chef/chef_fs/file_system/chef_server/policy_group_entry.rb new file mode 100644 index 0000000000..76ececbd5b --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/policy_group_entry.rb @@ -0,0 +1,137 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/already_exists_error' +require 'chef/chef_fs/file_system/not_found_error' +require 'chef/chef_fs/file_system/operation_failed_error' + +class Chef + module ChefFS + module FileSystem + module ChefServer + # Represents an entire policy group. + # Server path: /organizations/ORG/policy_groups/GROUP + # Repository path: policy_groups\GROUP.json + # Format: + # { + # "policies": { + # "a": { "revision_id": "1.0.0" } + # } + # } + class PolicyGroupEntry < RestListEntry + # delete is handled normally: + # DELETE /organizations/ORG/policy_groups/GROUP + + # read is handled normally: + # GET /organizations/ORG/policy_groups/GROUP + + # write is different. + # For each policy: + # - PUT /organizations/ORG/policy_groups/GROUP/policies/POLICY + # For each policy on the server but not the client: + # - DELETE /organizations/ORG/policy_groups/GROUP/policies/POLICY + # If the server has associations for a, b and c, + # And the client wants associations for a, x and y, + # We must PUT a, x and y + # And DELETE b and c + def write(file_contents) + # Parse the contents to ensure they are valid JSON + begin + object = Chef::JSONCompat.parse(file_contents) + rescue Chef::Exceptions::JSON::ParseError => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Parse error reading JSON creating child '#{name}': #{e}") + end + + if data_handler + object = data_handler.normalize_for_put(object, self) + data_handler.verify_integrity(object, self) do |error| + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, nil, "Error creating '#{name}': #{error}") + end + end + + begin + + # this should all get carted to PolicyGroupEntry#write. + + # the server demands the full policy data, but we want users' local policy_group documents to just + # have the data you'd get from GET /policy_groups/POLICY_GROUP. so we try to fetch that. + + # ordinarily this would be POST to the normal URL, but we do PUT to + # /organizations/{organization}/policy_groups/{policy_group}/policies/{policy_name} with the full + # policy data, for each individual policy. + policy_datas = {} + + object["policies"].each do |policy_name, policy_data| + policy_path = "/policies/#{policy_name}/revisions/#{policy_data["revision_id"]}" + + get_data = begin + rest.get(policy_path) + rescue Net::HTTPServerException => e + raise "Could not find policy '#{policy_name}'' with revision '#{policy_data["revision_id"]}'' on the server" + end + + # GET policy data + server_policy_data = Chef::JSONCompat.parse(get_data) + + # if it comes back 404, raise an Exception with "Policy file X does not exist with revision Y on the server" + + # otherwise, add it to the list of policyfile datas. + policy_datas[policy_name] = server_policy_data + end + + begin + existing_group = Chef::JSONCompat.parse(self.read) + rescue NotFoundError + # It's OK if the group doesn't already exist, just means no existing policies + end + + # now we have the fullpolicy data for each policies, which is what the PUT endpoint demands. + policy_datas.each do |policy_name, policy_data| + # PUT /organizations/ORG/policy_groups/GROUP/policies/NAME + rest.put("#{api_path}/policies/#{policy_name}", policy_data) + end + + # Now we need to remove any policies that are *not* in our current group. + if existing_group && existing_group['policies'] + (existing_group['policies'].keys - policy_datas.keys).each do |policy_name| + rest.delete("#{api_path}/policies/#{policy_name}") + end + end + + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Timeout creating '#{name}': #{e}") + rescue Net::HTTPServerException => e + # 404 = NotFoundError + if e.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) + # 409 = AlreadyExistsError + elsif $!.response.code == "409" + raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, self, e, "Failure creating '#{name}': #{path}/#{name} already exists") + # Anything else is unexpected (OperationFailedError) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Failure creating '#{name}': #{e.message}") + end + end + + self + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/policy_groups_dir.rb b/lib/chef/chef_fs/file_system/chef_server/policy_groups_dir.rb new file mode 100644 index 0000000000..0c061ebdb2 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/policy_groups_dir.rb @@ -0,0 +1,43 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/base_fs_dir' +require 'chef/chef_fs/file_system/chef_server/rest_list_entry' +require 'chef/chef_fs/file_system/not_found_error' +require 'chef/chef_fs/file_system/chef_server/policy_group_entry' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class PolicyGroupsDir < RestListDir + def make_child_entry(name, exists = nil) + PolicyGroupEntry.new(name, self, exists) + end + + def create_child(name, file_contents) + entry = make_child_entry(name, true) + entry.write(file_contents) + @children = nil + entry + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/policy_revision_entry.rb b/lib/chef/chef_fs/file_system/chef_server/policy_revision_entry.rb new file mode 100644 index 0000000000..7ea93278d0 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/policy_revision_entry.rb @@ -0,0 +1,25 @@ +require 'chef/chef_fs/file_system/chef_server/rest_list_entry' +require 'chef/chef_fs/data_handler/policy_data_handler' + +class Chef + module ChefFS + module FileSystem + module ChefServer + # /policies/NAME-REVISION.json + # Represents the actual data at /organizations/ORG/policies/NAME/revisions/REVISION + class PolicyRevisionEntry < RestListEntry + + # /policies/foo-1.0.0.json -> /policies/foo/revisions/1.0.0 + def api_path(options={}) + policy_name, revision_id = data_handler.name_and_revision(name) + "#{parent.api_path}/#{policy_name}/revisions/#{revision_id}" + end + + def write(file_contents) + raise OperationNotAllowedError.new(:write, self, nil, "cannot be updated: policy revisions are immutable once uploaded. If you want to change the policy, create a new revision with your changes") + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/rest_list_dir.rb b/lib/chef/chef_fs/file_system/chef_server/rest_list_dir.rb new file mode 100644 index 0000000000..84790190d1 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/rest_list_dir.rb @@ -0,0 +1,179 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/base_fs_dir' +require 'chef/chef_fs/file_system/chef_server/rest_list_entry' +require 'chef/chef_fs/file_system/not_found_error' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class RestListDir < BaseFSDir + def initialize(name, parent, api_path = nil, data_handler = nil) + super(name, parent) + @api_path = api_path || (parent.api_path == "" ? name : "#{parent.api_path}/#{name}") + @data_handler = data_handler + end + + attr_reader :api_path + attr_reader :data_handler + + def can_have_child?(name, is_dir) + name =~ /\.json$/ && !is_dir + end + + # + # When talking to a modern (12.0+) Chef server + # knife list / + # -> /nodes + # -> /policies + # -> /policy_groups + # -> /roles + # + # 12.0 or 12.1 will fail when you do this: + # knife list / --recursive + # Because it thinks /policies exists, and when it tries to list its children + # it gets a 404 (indicating it actually doesn't exist). + # + # With this change, knife list / --recursive will list /policies as a real, empty directory. + # + # Alternately, we could have done some sort of detection when we listed the top level + # and determined which endpoints the server would support, and returned only those. + # So you wouldn't see /policies in that case at all. + # The issue with that is there's no efficient way to do it because we can't find out + # the server version directly, and can't ask the server for a list of the endpoints it supports. + # + + + # + # Does GET /<api_path>, assumes the result is of the format: + # + # { + # "foo": "<api_path>/foo", + # "bar": "<api_path>/bar", + # } + # + # Children are foo.json and bar.json in this case. + # + def children + begin + # Grab the names of the children, append json, and make child entries + @children ||= root.get_json(api_path).keys.sort.map do |key| + make_child_entry("#{key}.json", true) + end + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "Timeout retrieving children: #{e}") + rescue Net::HTTPServerException => e + # 404 = NotFoundError + if $!.response.code == "404" + + if parent.is_a?(ChefServerRootDir) + # GET /organizations/ORG/<container> returned 404, but that just might be because + # we are talking to an older version of the server that doesn't support policies. + # Do GET /orgqanizations/ORG to find out if the org exists at all. + # TODO use server API version instead of a second network request. + begin + root.get_json(parent.api_path) + # Return empty list if the organization exists but /policies didn't work + [] + rescue Net::HTTPServerException => e + if e.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) + end + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "HTTP error retrieving children: #{e}") + end + else + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) + end + + # Anything else is unexpected (OperationFailedError) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "HTTP error retrieving children: #{e}") + end + end + end + + # + # Does POST <api_path> with file_contents + # + def create_child(name, file_contents) + # Parse the contents to ensure they are valid JSON + begin + object = Chef::JSONCompat.parse(file_contents) + rescue Chef::Exceptions::JSON::ParseError => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Parse error reading JSON creating child '#{name}': #{e}") + end + + # Create the child entry that will be returned + result = make_child_entry(name, true) + + # Normalize the file_contents before post (add defaults, etc.) + if data_handler + object = data_handler.normalize_for_post(object, result) + data_handler.verify_integrity(object, result) do |error| + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, nil, "Error creating '#{name}': #{error}") + end + end + + # POST /api_path with the normalized file_contents + begin + rest.post(api_path, object) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Timeout creating '#{name}': #{e}") + rescue Net::HTTPServerException => e + # 404 = NotFoundError + if e.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) + # 409 = AlreadyExistsError + elsif $!.response.code == "409" + raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, self, e, "Failure creating '#{name}': #{path}/#{name} already exists") + # Anything else is unexpected (OperationFailedError) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Failure creating '#{name}': #{e.message}") + end + end + + # Clear the cache of children so that if someone asks for children + # again, we will get it again + @children = nil + + result + end + + def org + parent.org + end + + def environment + parent.environment + end + + def rest + parent.rest + end + + def make_child_entry(name, exists = nil) + @children.select { |child| child.name == name }.first if @children + RestListEntry.new(name, self, exists) + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/rest_list_entry.rb b/lib/chef/chef_fs/file_system/chef_server/rest_list_entry.rb new file mode 100644 index 0000000000..4ceab42c1b --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/rest_list_entry.rb @@ -0,0 +1,187 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/base_fs_object' +require 'chef/chef_fs/file_system/not_found_error' +require 'chef/chef_fs/file_system/operation_failed_error' +require 'chef/role' +require 'chef/node' +require 'chef/json_compat' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class RestListEntry < BaseFSObject + def initialize(name, parent, exists = nil) + super(name, parent) + @exists = exists + end + + def data_handler + parent.data_handler + end + + def api_child_name + if name.length < 5 || name[-5,5] != ".json" + raise "Invalid name #{path}: must end in .json" + end + name[0,name.length-5] + end + + def api_path + "#{parent.api_path}/#{api_child_name}" + end + + def org + parent.org + end + + def environment + parent.environment + end + + def exists? + if @exists.nil? + begin + @exists = parent.children.any? { |child| child.name == name } + rescue Chef::ChefFS::FileSystem::NotFoundError + @exists = false + end + end + @exists + end + + def delete(recurse) + begin + rest.delete(api_path) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e, "Timeout deleting: #{e}") + rescue Net::HTTPServerException => e + if e.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e, "Timeout deleting: #{e}") + end + end + end + + def read + Chef::JSONCompat.to_json_pretty(minimize_value(_read_json)) + end + + def _read_json + begin + # Minimize the value (get rid of defaults) so the results don't look terrible + root.get_json(api_path) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "Timeout reading: #{e}") + rescue Net::HTTPServerException => e + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "HTTP error reading: #{e}") + end + end + end + + def chef_object + # REST will inflate the Chef object using json_class + data_handler.json_class.json_create(read) + end + + def minimize_value(value) + data_handler.minimize(data_handler.normalize(value, self), self) + end + + def compare_to(other) + # TODO this pair of reads can be parallelized + + # Grab the other value + begin + other_value_json = other.read + rescue Chef::ChefFS::FileSystem::NotFoundError + return [ nil, nil, :none ] + end + + # Grab this value + begin + value = _read_json + rescue Chef::ChefFS::FileSystem::NotFoundError + return [ false, :none, other_value_json ] + end + + # Minimize (and normalize) both values for easy and beautiful diffs + value = minimize_value(value) + value_json = Chef::JSONCompat.to_json_pretty(value) + begin + other_value = Chef::JSONCompat.parse(other_value_json) + rescue Chef::Exceptions::JSON::ParseError => e + Chef::Log.warn("Parse error reading #{other.path_for_printing} as JSON: #{e}") + return [ nil, value_json, other_value_json ] + end + other_value = minimize_value(other_value) + other_value_json = Chef::JSONCompat.to_json_pretty(other_value) + + [ value == other_value, value_json, other_value_json ] + end + + def rest + parent.rest + end + + def write(file_contents) + begin + object = Chef::JSONCompat.parse(file_contents) + rescue Chef::Exceptions::JSON::ParseError => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "Parse error reading JSON: #{e}") + end + + if data_handler + object = data_handler.normalize_for_put(object, self) + data_handler.verify_integrity(object, self) do |error| + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, nil, "#{error}") + end + end + + begin + rest.put(api_path, object) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "Timeout writing: #{e}") + rescue Net::HTTPServerException => e + if e.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "HTTP error writing: #{e}") + end + end + end + + def api_error_text(response) + begin + Chef::JSONCompat.parse(response.body)['error'].join("\n") + rescue + response.body + end + end + end + + end + end + end +end |