diff options
Diffstat (limited to 'lib/chef/chef_fs/file_system')
21 files changed, 1491 insertions, 0 deletions
diff --git a/lib/chef/chef_fs/file_system/base_fs_dir.rb b/lib/chef/chef_fs/file_system/base_fs_dir.rb new file mode 100644 index 0000000000..74038f481b --- /dev/null +++ b/lib/chef/chef_fs/file_system/base_fs_dir.rb @@ -0,0 +1,47 @@ +# +# 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/nonexistent_fs_object' + +class Chef + module ChefFS + module FileSystem + class BaseFSDir < BaseFSObject + def initialize(name, parent) + super + end + + def dir? + true + end + + # Override child(name) to provide a child object by name without the network read + def child(name) + children.select { |child| child.name == name }.first || NonexistentFSObject.new(name, self) + end + + def can_have_child?(name, is_dir) + true + end + + # Abstract: children + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/base_fs_object.rb b/lib/chef/chef_fs/file_system/base_fs_object.rb new file mode 100644 index 0000000000..855892fc89 --- /dev/null +++ b/lib/chef/chef_fs/file_system/base_fs_object.rb @@ -0,0 +1,121 @@ +# +# 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/path_utils' + +class Chef + module ChefFS + module FileSystem + class BaseFSObject + def initialize(name, parent) + @parent = parent + @name = name + if parent + @path = Chef::ChefFS::PathUtils::join(parent.path, name) + else + if name != '' + raise ArgumentError, "Name of root object must be empty string: was '#{name}' instead" + end + @path = '/' + end + end + + attr_reader :name + attr_reader :parent + attr_reader :path + + def root + parent ? parent.root : self + end + + def path_for_printing + if parent + parent_path = parent.path_for_printing + if parent_path == '.' + name + else + Chef::ChefFS::PathUtils::join(parent.path_for_printing, name) + end + else + name + end + end + + def dir? + false + end + + def exists? + true + end + + def child(name) + NonexistentFSObject.new(name, self) + end + + # Override can_have_child? to report whether a given file *could* be added + # to this directory. (Some directories can't have subdirs, some can only have .json + # files, etc.) + def can_have_child?(name, is_dir) + false + end + + # Override this if you have a special comparison algorithm that can tell + # you whether this entry is the same as another--either a quicker or a + # more reliable one. Callers will use this to decide whether to upload, + # download or diff an object. + # + # You should not override this if you're going to do the standard + # +self.read == other.read+. If you return +nil+, the caller will call + # +other.compare_to(you)+ instead. Give them a chance :) + # + # ==== Parameters + # + # * +other+ - the entry to compare to + # + # ==== Returns + # + # * +[ are_same, value, other_value ]+ + # +are_same+ may be +true+, +false+ or +nil+ (which means "don't know"). + # +value+ and +other_value+ must either be the text of +self+ or +other+, + # +:none+ (if the entry does not exist or has no value) or +nil+ if the + # value was not retrieved. + # * +nil+ if a definitive answer cannot be had and nothing was retrieved. + # + # ==== Example + # + # are_same, value, other_value = entry.compare_to(other) + # if are_same.nil? + # are_same, other_value, value = other.compare_to(entry) + # end + # if are_same.nil? + # value = entry.read if value.nil? + # other_value = entry.read if other_value.nil? + # are_same = (value == other_value) + # end + def compare_to(other) + return nil + end + + # Important directory attributes: name, parent, path, root + # Overridable attributes: dir?, child(name), path_for_printing + # Abstract: read, write, delete, children + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb new file mode 100644 index 0000000000..87d904e830 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb @@ -0,0 +1,109 @@ +# +# 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/file_system_entry' +require 'chef/cookbook/chefignore' +require 'chef/cookbook/cookbook_version_loader' +require 'chef/node' +require 'chef/role' +require 'chef/environment' +require 'chef/data_bag_item' +require 'chef/client' + +class Chef + module ChefFS + module FileSystem + # ChefRepositoryFileSystemEntry works just like FileSystemEntry, + # except it pretends files in /cookbooks/chefignore don't exist + # and it can inflate Chef objects + class ChefRepositoryFileSystemEntry < FileSystemEntry + def initialize(name, parent, file_path = nil) + super(name, parent, file_path) + # Load /cookbooks/chefignore + if name == "cookbooks" && path == "/cookbooks" # We check name first because it's a faster fail than path + @chefignore = Chef::Cookbook::Chefignore.new(self.file_path) + # If we are a cookbook or a cookbook subdirectory, empty directories + # underneath us are ignored (since they cannot be uploaded) + elsif parent && parent.name === "cookbooks" && parent.path == "/cookbooks" + @ignore_empty_directories = true + elsif parent && parent.ignore_empty_directories? + @ignore_empty_directories = true + end + end + + attr_reader :chefignore + + def ignore_empty_directories? + @ignore_empty_directories + end + + def chef_object + begin + if parent.path == "/cookbooks" + loader = Chef::Cookbook::CookbookVersionLoader.new(file_path, parent.chefignore) + loader.load_cookbooks + return loader.cookbook_version + end + + # Otherwise the information to inflate the object, is in the file (json_class). + return Chef::JSONCompat.from_json(read) + rescue + Chef::Log.error("Could not read #{path_for_printing} into a Chef object: #{$!}") + end + nil + end + + def children + @children ||= Dir.entries(file_path).select { |entry| entry != '.' && entry != '..' && !ignored?(entry) }. + map { |entry| ChefRepositoryFileSystemEntry.new(entry, self) } + end + + attr_reader :chefignore + + private + + def ignored?(child_name) + # empty directories inside a cookbook are ignored + if ignore_empty_directories? + child_path = PathUtils.join(file_path, child_name) + if File.directory?(child_path) && Dir.entries(child_path) == [ '.', '..' ] + return true + end + end + + ignorer = self + begin + if ignorer.chefignore + # Grab the path from entry to child + path_to_child = child_name + child = self + while child != ignorer + path_to_child = PathUtils.join(child.name, path_to_child) + child = child.parent + end + # Check whether that relative path is ignored + return ignorer.chefignore.ignored?(path_to_child) + end + ignorer = ignorer.parent + end while ignorer + end + + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb new file mode 100644 index 0000000000..fdad68003c --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb @@ -0,0 +1,31 @@ +# +# 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_repository_file_system_entry' + +class Chef + module ChefFS + module FileSystem + class ChefRepositoryFileSystemRootDir < ChefRepositoryFileSystemEntry + def initialize(file_path) + super("", nil, file_path) + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server_root_dir.rb b/lib/chef/chef_fs/file_system/chef_server_root_dir.rb new file mode 100644 index 0000000000..d3c217d11c --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server_root_dir.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_dir' +require 'chef/chef_fs/file_system/rest_list_dir' +require 'chef/chef_fs/file_system/cookbooks_dir' +require 'chef/chef_fs/file_system/data_bags_dir' +require 'chef/chef_fs/file_system/nodes_dir' + +class Chef + module ChefFS + module FileSystem + class ChefServerRootDir < BaseFSDir + def initialize(root_name, chef_config, repo_mode) + 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 = repo_mode + @root_name = root_name + end + + attr_reader :chef_server_url + attr_reader :chef_username + attr_reader :chef_private_key + attr_reader :environment + attr_reader :repo_mode + + def rest + Chef::REST.new(chef_server_url, chef_username, chef_private_key) + end + + def api_path + "" + end + + def path_for_printing + "#{@root_name}/" + end + + def can_have_child?(name, is_dir) + is_dir && children.any? { |child| child.name == name } + end + + def children + @children ||= begin + result = [ + CookbooksDir.new(self), + DataBagsDir.new(self), + RestListDir.new("environments", self), + RestListDir.new("roles", self) + ] + if repo_mode == 'everything' + result += [ + RestListDir.new("clients", self), + NodesDir.new(self), + RestListDir.new("users", self) + ] + end + result.sort_by { |child| child.name } + end + end + + # Yeah, sorry, I'm not putting delete on this thing. + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/cookbook_dir.rb b/lib/chef/chef_fs/file_system/cookbook_dir.rb new file mode 100644 index 0000000000..e87d5dd49d --- /dev/null +++ b/lib/chef/chef_fs/file_system/cookbook_dir.rb @@ -0,0 +1,188 @@ +# +# 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/rest_list_dir' +require 'chef/chef_fs/file_system/cookbook_subdir' +require 'chef/chef_fs/file_system/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 + class CookbookDir < BaseFSDir + def initialize(name, parent, versions = nil) + super(name, parent) + @versions = versions + end + + attr_reader :versions + + 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 => { } + } + + def add_child(child) + @children << child + end + + def api_path + "#{parent.api_path}/#{name}/_latest" + end + + def child(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 + result = children.select { |child| child.name == name }.first + return result if result + rescue Chef::ChefFS::FileSystem::NotFoundError + end + return NonexistentFSObject.new(name, self) + end + + def can_have_child?(name, is_dir) + # A cookbook's root may not have directories unless they are segment directories + if is_dir + return name != 'root_files' && + COOKBOOK_SEGMENT_INFO.keys.any? { |segment| segment.to_s == name } + end + 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 + end + @children + 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, path_for_printing + end + + def exists? + if !@versions + child = parent.children.select { |child| child.name == name }.first + @versions = child.versions if child + end + !!@versions + 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) do + are_same = false + end + [ are_same, nil, nil ] + end + + def copy_from(other) + parent.upload_cookbook_from(other) + 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(@could_not_get_chef_object), "#{path_for_printing} not found" + 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 ||= rest.get_rest(api_path) + ensure + Chef::Config[:http_retry_count] = old_retry_count + end + rescue Net::HTTPServerException + if $!.response.code == "404" + @could_not_get_chef_object = $! + raise Chef::ChefFS::FileSystem::NotFoundError.new(@could_not_get_chef_object), "#{path_for_printing} not found" + else + raise + 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 + if $!.response.code == "500" + @could_not_get_chef_object = $! + raise Chef::ChefFS::FileSystem::NotFoundError.new(@could_not_get_chef_object), "#{path_for_printing} not found" + else + raise + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/cookbook_file.rb b/lib/chef/chef_fs/file_system/cookbook_file.rb new file mode 100644 index 0000000000..baa71f5d9e --- /dev/null +++ b/lib/chef/chef_fs/file_system/cookbook_file.rb @@ -0,0 +1,78 @@ +# +# 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 'digest/md5' + +class Chef + module ChefFS + module FileSystem + class CookbookFile < BaseFSObject + def initialize(name, parent, file) + super(name, parent) + @file = file + end + + attr_reader :file + + def checksum + file[:checksum] + end + + def read + old_sign_on_redirect = rest.sign_on_redirect + rest.sign_on_redirect = false + begin + rest.get_rest(file[:url]) + ensure + rest.sign_on_redirect = old_sign_on_redirect + 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) + begin + Digest::MD5.hexdigest(value) + rescue Chef::ChefFS::FileSystem::NotFoundError + nil + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/cookbook_subdir.rb b/lib/chef/chef_fs/file_system/cookbook_subdir.rb new file mode 100644 index 0000000000..73c709e01e --- /dev/null +++ b/lib/chef/chef_fs/file_system/cookbook_subdir.rb @@ -0,0 +1,54 @@ +# +# 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 + 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 rest + parent.rest + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/cookbooks_dir.rb b/lib/chef/chef_fs/file_system/cookbooks_dir.rb new file mode 100644 index 0000000000..9249b42aaa --- /dev/null +++ b/lib/chef/chef_fs/file_system/cookbooks_dir.rb @@ -0,0 +1,68 @@ +# +# 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/rest_list_dir' +require 'chef/chef_fs/file_system/cookbook_dir' + +class Chef + module ChefFS + module FileSystem + class CookbooksDir < RestListDir + def initialize(parent) + super("cookbooks", parent) + end + + def child(name) + result = @children.select { |child| child.name == name }.first if @children + result || CookbookDir.new(name, self) + end + + def children + @children ||= rest.get_rest(api_path).map { |key, value| CookbookDir.new(key, self, value) } + end + + def create_child_from(other) + upload_cookbook_from(other) + end + + def upload_cookbook_from(other) + other_cookbook_version = other.chef_object + # TODO this only works on the file system. And it can't be broken into + # pieces. + begin + uploader = Chef::CookbookUploader.new(other_cookbook_version, other.parent.file_path) + uploader.upload_cookbooks + rescue Net::HTTPServerException => e + case e.response.code + when "409" + ui.error "Version #{other_cookbook_version.version} of cookbook #{other_cookbook_version.name} is frozen. Use --force to override." + Chef::Log.debug(e) + raise Exceptions::CookbookFrozen + else + raise + end + end + end + + def can_have_child?(name, is_dir) + is_dir + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/data_bag_dir.rb b/lib/chef/chef_fs/file_system/data_bag_dir.rb new file mode 100644 index 0000000000..41fb5dfc63 --- /dev/null +++ b/lib/chef/chef_fs/file_system/data_bag_dir.rb @@ -0,0 +1,78 @@ +# +# 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/rest_list_dir' +require 'chef/chef_fs/file_system/data_bag_item' +require 'chef/chef_fs/file_system/not_found_error' +require 'chef/chef_fs/file_system/must_delete_recursively_error' + +class Chef + module ChefFS + module FileSystem + class DataBagDir < RestListDir + def initialize(name, parent, exists = nil) + super(name, parent) + @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, "#{path_for_printing} not found" + end + + def exists? + if @exists.nil? + @exists = parent.children.any? { |child| child.name == name } + end + @exists + end + + def create_child(name, file_contents) + json = Chef::JSONCompat.from_json(file_contents).to_hash + id = name[0,name.length-5] + if json.include?('id') && json['id'] != id + raise "ID in #{path_for_printing}/#{name} must be '#{id}' (is '#{json['id']}')" + end + rest.post_rest(api_path, json) + _make_child_entry(name, true) + end + + def _make_child_entry(name, exists = nil) + DataBagItem.new(name, self, exists) + end + + def delete(recurse) + if !recurse + raise Chef::ChefFS::FileSystem::MustDeleteRecursivelyError.new, "#{path_for_printing} must be deleted recursively" + end + begin + rest.delete_rest(api_path) + rescue Net::HTTPServerException + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found" + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/data_bag_item.rb b/lib/chef/chef_fs/file_system/data_bag_item.rb new file mode 100644 index 0000000000..2f6eb15232 --- /dev/null +++ b/lib/chef/chef_fs/file_system/data_bag_item.rb @@ -0,0 +1,59 @@ +# +# 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/rest_list_entry' + +class Chef + module ChefFS + module FileSystem + class DataBagItem < RestListEntry + def initialize(name, parent, exists = nil) + super(name, parent, exists) + end + + def write(file_contents) + # Write is just a little tiny bit different for data bags: + # you set raw_data in the JSON instead of putting the items + # in the top level. + json = Chef::JSONCompat.from_json(file_contents).to_hash + id = name[0,name.length-5] # Strip off the .json from the end + if json['id'] != id + raise "Id in #{path_for_printing}/#{name} must be '#{id}' (is '#{json['id']}')" + end + begin + data_bag = parent.name + json = { + "name" => "data_bag_item_#{data_bag}_#{id}", + "json_class" => "Chef::DataBagItem", + "chef_type" => "data_bag_item", + "data_bag" => data_bag, + "raw_data" => json + } + rest.put_rest(api_path, json) + rescue Net::HTTPServerException + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found" + else + raise + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/data_bags_dir.rb b/lib/chef/chef_fs/file_system/data_bags_dir.rb new file mode 100644 index 0000000000..6eca990545 --- /dev/null +++ b/lib/chef/chef_fs/file_system/data_bags_dir.rb @@ -0,0 +1,66 @@ +# +# 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/rest_list_dir' +require 'chef/chef_fs/file_system/data_bag_dir' + +class Chef + module ChefFS + module FileSystem + class DataBagsDir < RestListDir + def initialize(parent) + super("data_bags", parent, "data") + end + + def child(name) + result = @children.select { |child| child.name == name }.first if @children + result || DataBagDir.new(name, self) + end + + def children + begin + @children ||= rest.get_rest(api_path).keys.map do |entry| + DataBagDir.new(entry, self, true) + end + rescue Net::HTTPServerException + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found" + else + raise + end + end + end + + def can_have_child?(name, is_dir) + is_dir + end + + def create_child(name, file_contents) + begin + rest.post_rest(api_path, { 'name' => name }) + rescue Net::HTTPServerException + if $!.response.code != "409" + raise + end + end + DataBagDir.new(name, self, true) + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/file_system_entry.rb b/lib/chef/chef_fs/file_system/file_system_entry.rb new file mode 100644 index 0000000000..a86e0cb82a --- /dev/null +++ b/lib/chef/chef_fs/file_system/file_system_entry.rb @@ -0,0 +1,90 @@ +# +# 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/rest_list_dir' +require 'chef/chef_fs/file_system/not_found_error' +require 'chef/chef_fs/path_utils' +require 'fileutils' + +class Chef + module ChefFS + module FileSystem + class FileSystemEntry < BaseFSDir + def initialize(name, parent, file_path = nil) + super(name, parent) + @file_path = file_path || "#{parent.file_path}/#{name}" + end + + attr_reader :file_path + + def path_for_printing + Chef::ChefFS::PathUtils::relative_to(file_path, File.expand_path(Dir.pwd)) + end + + def children + begin + @children ||= Dir.entries(file_path).select { |entry| entry != '.' && entry != '..' }.map { |entry| FileSystemEntry.new(entry, self) } + rescue Errno::ENOENT + raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{file_path} not found" + end + end + + def create_child(child_name, file_contents=nil) + result = FileSystemEntry.new(child_name, self) + if file_contents + result.write(file_contents) + else + Dir.mkdir(result.file_path) + end + result + end + + def dir? + File.directory?(file_path) + end + + def delete(recurse) + if dir? + if recurse + FileUtils.rm_rf(file_path) + else + File.rmdir(file_path) + end + else + File.delete(file_path) + end + end + + def read + begin + File.open(file_path, "rb") {|f| f.read} + rescue Errno::ENOENT + raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{file_path} not found" + end + end + + def write(content) + File.open(file_path, 'wb') do |file| + file.write(content) + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/file_system_error.rb b/lib/chef/chef_fs/file_system/file_system_error.rb new file mode 100644 index 0000000000..a461221108 --- /dev/null +++ b/lib/chef/chef_fs/file_system/file_system_error.rb @@ -0,0 +1,31 @@ +# +# 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. +# + +class Chef + module ChefFS + module FileSystem + class FileSystemError < StandardError + def initialize(cause = nil) + @cause = cause + end + + attr_reader :cause + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/file_system_root_dir.rb b/lib/chef/chef_fs/file_system/file_system_root_dir.rb new file mode 100644 index 0000000000..afbf7b1901 --- /dev/null +++ b/lib/chef/chef_fs/file_system/file_system_root_dir.rb @@ -0,0 +1,31 @@ +# +# 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/file_system_entry' + +class Chef + module ChefFS + module FileSystem + class FileSystemRootDir < FileSystemEntry + def initialize(file_path) + super("", nil, file_path) + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/must_delete_recursively_error.rb b/lib/chef/chef_fs/file_system/must_delete_recursively_error.rb new file mode 100644 index 0000000000..d247a5b4ed --- /dev/null +++ b/lib/chef/chef_fs/file_system/must_delete_recursively_error.rb @@ -0,0 +1,31 @@ +# +# 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/file_system_error' + +class Chef + module ChefFS + module FileSystem + class MustDeleteRecursivelyError < FileSystemError + def initialize(cause = nil) + super(cause) + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/nodes_dir.rb b/lib/chef/chef_fs/file_system/nodes_dir.rb new file mode 100644 index 0000000000..4dfbf6d850 --- /dev/null +++ b/lib/chef/chef_fs/file_system/nodes_dir.rb @@ -0,0 +1,47 @@ +# +# 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/rest_list_entry' +require 'chef/chef_fs/file_system/not_found_error' + +class Chef + module ChefFS + module FileSystem + class NodesDir < RestListDir + def initialize(parent) + super("nodes", parent) + end + + # Override children to respond to environment + def children + @children ||= begin + env_api_path = environment ? "environments/#{environment}/#{api_path}" : api_path + rest.get_rest(env_api_path).keys.map { |key| RestListEntry.new("#{key}.json", self, true) } + rescue Net::HTTPServerException + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found" + else + raise + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/nonexistent_fs_object.rb b/lib/chef/chef_fs/file_system/nonexistent_fs_object.rb new file mode 100644 index 0000000000..dc82e83b0d --- /dev/null +++ b/lib/chef/chef_fs/file_system/nonexistent_fs_object.rb @@ -0,0 +1,40 @@ +# +# 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' + +class Chef + module ChefFS + module FileSystem + class NonexistentFSObject < BaseFSObject + def initialize(name, parent) + super + end + + def exists? + false + end + + def read + raise Chef::ChefFS::FileSystem::NotFoundError, "Nonexistent #{path_for_printing}" + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/not_found_error.rb b/lib/chef/chef_fs/file_system/not_found_error.rb new file mode 100644 index 0000000000..0b608f1abf --- /dev/null +++ b/lib/chef/chef_fs/file_system/not_found_error.rb @@ -0,0 +1,31 @@ +# +# 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/file_system_error' + +class Chef + module ChefFS + module FileSystem + class NotFoundError < FileSystemError + def initialize(cause = nil) + super(cause) + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/rest_list_dir.rb b/lib/chef/chef_fs/file_system/rest_list_dir.rb new file mode 100644 index 0000000000..0e8db4d7b9 --- /dev/null +++ b/lib/chef/chef_fs/file_system/rest_list_dir.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_dir' +require 'chef/chef_fs/file_system/rest_list_entry' +require 'chef/chef_fs/file_system/not_found_error' + +class Chef + module ChefFS + module FileSystem + class RestListDir < BaseFSDir + def initialize(name, parent, api_path = nil) + super(name, parent) + @api_path = api_path || (parent.api_path == "" ? name : "#{parent.api_path}/#{name}") + end + + attr_reader :api_path + + def child(name) + result = @children.select { |child| child.name == name }.first if @children + result ||= can_have_child?(name, false) ? + _make_child_entry(name) : NonexistentFSObject.new(name, self) + end + + def can_have_child?(name, is_dir) + name =~ /\.json$/ && !is_dir + end + + def children + begin + @children ||= rest.get_rest(api_path).keys.map do |key| + _make_child_entry("#{key}.json", true) + end + rescue Net::HTTPServerException + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found" + else + raise + end + end + end + + # NOTE if you change this significantly, you will likely need to change + # DataBagDir.create_child as well. + def create_child(name, file_contents) + json = Chef::JSONCompat.from_json(file_contents).to_hash + base_name = name[0,name.length-5] + if json.include?('name') && json['name'] != base_name + raise "Name in #{path_for_printing}/#{name} must be '#{base_name}' (is '#{json['name']}')" + end + rest.post_rest(api_path, json) + _make_child_entry(name, true) + end + + def environment + parent.environment + end + + def rest + parent.rest + end + + def _make_child_entry(name, exists = nil) + RestListEntry.new(name, self, exists) + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/rest_list_entry.rb b/lib/chef/chef_fs/file_system/rest_list_entry.rb new file mode 100644 index 0000000000..dd504ef341 --- /dev/null +++ b/lib/chef/chef_fs/file_system/rest_list_entry.rb @@ -0,0 +1,123 @@ +# +# 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/role' +require 'chef/node' + +class Chef + module ChefFS + module FileSystem + class RestListEntry < BaseFSObject + def initialize(name, parent, exists = nil) + super(name, parent) + @exists = exists + end + + def api_path + if name.length < 5 || name[-5,5] != ".json" + raise "Invalid name #{path}: must end in .json" + end + api_child_name = name[0,name.length-5] + "#{parent.api_path}/#{api_child_name}" + 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_rest(api_path) + rescue Net::HTTPServerException + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found" + else + raise + end + end + end + + def read + Chef::JSONCompat.to_json_pretty(chef_object.to_hash) + end + + def chef_object + begin + # REST will inflate the Chef object using json_class + rest.get_rest(api_path) + rescue Net::HTTPServerException + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found" + else + raise + end + end + end + + def compare_to(other) + begin + other_value = other.read + rescue Chef::ChefFS::FileSystem::NotFoundError + return [ nil, nil, :none ] + end + begin + value = chef_object.to_hash + rescue Chef::ChefFS::FileSystem::NotFoundError + return [ false, :none, other_value ] + end + are_same = (value == Chef::JSONCompat.from_json(other_value, :create_additions => false)) + [ are_same, Chef::JSONCompat.to_json_pretty(value), other_value ] + end + + def rest + parent.rest + end + + def write(file_contents) + json = Chef::JSONCompat.from_json(file_contents).to_hash + base_name = name[0,name.length-5] + if json['name'] != base_name + raise "Name in #{path_for_printing}/#{name} must be '#{base_name}' (is '#{json['name']}')" + end + begin + rest.put_rest(api_path, json) + rescue Net::HTTPServerException + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found" + else + raise + end + end + end + end + end + end +end |