diff --git a/lib/fog/aws.rb b/lib/fog/aws.rb index 5c6d530f0f..8d8ebe2f82 100644 --- a/lib/fog/aws.rb +++ b/lib/fog/aws.rb @@ -36,6 +36,7 @@ module AWS autoload :DataPipeline, File.expand_path('../aws/data_pipeline', __FILE__) autoload :DynamoDB, File.expand_path('../aws/dynamodb', __FILE__) autoload :ECS, File.expand_path('../aws/ecs', __FILE__) + autoload :EFS, File.expand_path('../aws/efs', __FILE__) autoload :ELB, File.expand_path('../aws/elb', __FILE__) autoload :EMR, File.expand_path('../aws/emr', __FILE__) autoload :ElasticBeanstalk, File.expand_path('../aws/beanstalk', __FILE__) @@ -66,6 +67,7 @@ module AWS service(:dynamodb, 'DynamoDB') service(:elasticache, 'Elasticache') service(:ecs, 'ECS') + service(:efs, 'EFS') service(:elb, 'ELB') service(:emr, 'EMR') service(:federation, 'Federation') diff --git a/lib/fog/aws/efs.rb b/lib/fog/aws/efs.rb new file mode 100644 index 0000000000..dcab824385 --- /dev/null +++ b/lib/fog/aws/efs.rb @@ -0,0 +1,147 @@ +module Fog + module AWS + class EFS < Fog::Service + extend Fog::AWS::CredentialFetcher::ServiceMethods + + class FileSystemInUse < Fog::Errors::Error; end + + requires :aws_access_key_id, :aws_secret_access_key + + model_path 'fog/aws/models/efs' + request_path 'fog/aws/requests/efs' + + model :file_system + collection :file_systems + + request :create_file_system + request :delete_file_system + request :describe_file_systems + + class Mock + def self.data + @data ||= Hash.new do |hash, region| + hash[region] = Hash.new do |region_hash, key| + region_hash[key] = { + :file_systems => {} + } + end + end + end + + def self.reset + @data = nil + end + + def data + self.class.data[@region][@aws_access_key_id] + end + + def reset + self.class.reset + end + + attr_accessor :region + + def initialize(options={}) + @region = options[:region] || "us-east-1" + end + end + + class Real + include Fog::AWS::CredentialFetcher::ConnectionMethods + + def initialize(options={}) + @connection_options = options[:connection_options] || {} + @instrumentor = options[:instrumentor] + @instrumentor_name = options[:instrumentor_name] || 'fog.aws.efs' + + @region = 'us-east-1' + @host = options[:host] || "elasticfilesystem.#{@region}.amazonaws.com" + @port = options[:port] || 443 + @scheme = options[:scheme] || "https" + @persistent = options[:persistent] || false + @connection = Fog::XML::Connection.new("#{@scheme}://#{@host}:#{@port}#{@path}", @persistent, @connection_options) + @version = options[:version] || '2015-02-01' + @path = options[:path] || "/#{@version}/" + + setup_credentials(options) + end + + def reload + @connection.reset + end + + def setup_credentials(options) + @aws_access_key_id = options[:aws_access_key_id] + @aws_secret_access_key = options[:aws_secret_access_key] + @aws_session_token = options[:aws_session_token] + @aws_credentials_expire_at = options[:aws_credentials_expire_at] + + #global services that have no region are signed with the us-east-1 region + #the only exception is GovCloud, which requires the region to be explicitly specified as us-gov-west-1 + @signer = Fog::AWS::SignatureV4.new(@aws_access_key_id, @aws_secret_access_key, @region, 'elasticfilesystem') + end + + def request(params) + refresh_credentials_if_expired + idempotent = params.delete(:idempotent) + parser = params.delete(:parser) + expects = params.delete(:expects) || 200 + path = @path + params.delete(:path) + method = params.delete(:method) || 'GET' + request_body = Fog::JSON.encode(params) + + body, headers = Fog::AWS.signed_params_v4( + params, + { + 'Content-Type' => "application/x-amz-json-1.0", + }, + { + :host => @host, + :path => path, + :port => @port, + :version => @version, + :signer => @signer, + :aws_session_token => @aws_session_token, + :method => method, + :body => request_body + } + ) + + if @instrumentor + @instrumentor.instrument("#{@instrumentor_name}.request", params) do + _request(body, headers, idempotent, parser, method, path, expects) + end + else + _request(body, headers, idempotent, parser, method, path, expects) + end + end + + def _request(body, headers, idempotent, parser, method, path, expects) + response = @connection.request({ + :body => body, + :expects => expects, + :idempotent => idempotent, + :headers => headers, + :method => method, + :parser => parser, + :path => path + }) + unless response.body.empty? + response.body = Fog::JSON.decode(response.body) + end + response + rescue Excon::Errors::HTTPStatusError => error + match = Fog::AWS::Errors.match_error(error) + raise if match.empty? + raise case match[:message] + when /invalid file system id/i + Fog::AWS::EFS::NotFound.slurp(error, match[:message]) + else + Fog::AWS::EFS::Error.slurp(error, "#{match[:code]} => #{match[:message]}") + end + end + end + end + end +end diff --git a/lib/fog/aws/models/efs/file_system.rb b/lib/fog/aws/models/efs/file_system.rb new file mode 100644 index 0000000000..354b5b653d --- /dev/null +++ b/lib/fog/aws/models/efs/file_system.rb @@ -0,0 +1,36 @@ +module Fog + module AWS + class EFS + class FileSystem < Fog::Model + identity :id, :aliases => 'FileSystemId' + + attribute :owner_id, :aliases => 'OwnerId' + attribute :creation_token, :aliases => 'CreationToken' + attribute :performance_mode, :aliases => 'PerformanceMode' + attribute :creation_time, :aliases => 'CreationTime' + attribute :state, :aliases => 'LifeCycleState' + attribute :name, :aliases => 'Name' + attribute :number_of_mount_targets, :aliases => 'NumberOfMountTargets' + attribute :size_in_bytes, :aliases => 'SizeInBytes' + + def destroy + requires :identity + + service.delete_file_system(:id => self.identity) + + true + end + + def save + params = { + :creation_token => self.creation_token || Fog::Mock.random_hex(32) + } + + params.merge!(:performance_mode => self.performance_mode) if self.performance_mode + + merge_attributes(service.create_file_system(:creation_token => self.creation_token).body) + end + end + end + end +end diff --git a/lib/fog/aws/models/efs/file_systems.rb b/lib/fog/aws/models/efs/file_systems.rb new file mode 100644 index 0000000000..e7f831040b --- /dev/null +++ b/lib/fog/aws/models/efs/file_systems.rb @@ -0,0 +1,23 @@ +require 'fog/aws/models/efs/file_system' + +module Fog + module AWS + class EFS + class FileSystems < Fog::Collection + model Fog::AWS::EFS::FileSystem + + def all + data = service.describe_file_systems.body["FileSystems"] + load(data) + end + + def get(identity) + data = service.describe_file_systems(:id => identity).body + new(data) + rescue Fog::AWS::EFS::NotFound + nil + end + end + end + end +end diff --git a/lib/fog/aws/requests/efs/create_file_system.rb b/lib/fog/aws/requests/efs/create_file_system.rb new file mode 100644 index 0000000000..856197ce37 --- /dev/null +++ b/lib/fog/aws/requests/efs/create_file_system.rb @@ -0,0 +1,51 @@ +module Fog + module AWS + class EFS + class Real + # Create a new, empty file system + # http://docs.aws.amazon.com/efs/latest/ug/API_CreateFileSystem.html + # ==== Parameters + # * CreationToken <~String> - String of up to 64 ASCII characters. Amazon EFS uses this to ensure idempotent creation. + # * PerformanceMode <~String> - (Optional) The PerformanceMode of the file system. We recommend generalPurpose performance mode for most file systems. File systems using the maxIO performance mode can scale to higher levels of aggregate throughput and operations per second with a tradeoff of slightly higher latencies for most file operations. This can't be changed after the file system has been created. + # ==== Returns + # * response<~Excon::Response> + # * body<~Hash> + def create_file_system(options={}) + request({ + :path => "file-systems", + :method => 'POST', + :expects => 201, + 'CreationToken' => options[:creation_token], + 'PerformanceMode' => options[:performance_mode] || 'generalPurpose' + }) + end + end + + class Mock + def create_file_system(options={}) + response = Excon::Response.new + + id = "fs-#{Fog::Mock.random_letters(8)}" + file_system = { + "OwnerId" => Fog::AWS::Mock.owner_id, + "CreationToken" => options[:creation_token], + "PerformanceMode" => options[:performance_mode] || "generalPurpose", + "FileSystemId" => id, + "CreationTime" => Time.now.to_i.to_f, + "LifeCycleState" => "creating", + "NumberOfMountTargets" => 0, + "SizeInBytes" => { + "Value" => 1024, + "Timestamp" => Time.now.to_i.to_f + } + } + + self.data[:file_systems][id] = file_system + response.body = file_system + response.status = 201 + response + end + end + end + end +end diff --git a/lib/fog/aws/requests/efs/delete_file_system.rb b/lib/fog/aws/requests/efs/delete_file_system.rb new file mode 100644 index 0000000000..5733782698 --- /dev/null +++ b/lib/fog/aws/requests/efs/delete_file_system.rb @@ -0,0 +1,46 @@ +module Fog + module AWS + class EFS + class Real + # Delete a file system + # http://docs.aws.amazon.com/efs/latest/ug/API_DeleteFileSystem.html + # ==== Parameters + # * FileSystemId <~String> - ID of the file system you want to delete. + # ==== Response + # * response<~Excon::Response> + # * body - Empty + # * status - 204 + def delete_file_system(options={}) + id = options.delete(:id) + request({ + :path => "file-systems/#{id}", + :method => 'DELETE', + :expects => 204, + 'CreationToken' => options[:creation_token], + 'PerformanceMode' => options[:performance_mode] || 'generalPurpose' + }) + end + end + + class Mock + def delete_file_system(options={}) + id = options.delete(:id) + + unless file_system = self.data[:file_systems][id] + raise Fog::AWS::EFS::NotFound.new("invalid file system ID: #{id}") + end + + if file_system["NumberOfMountTargets"] > 0 + raise Fog::AWS::EFS::FileSystemInUse.new("file system still has active mount targets") + end + + self.data[:file_systems].delete(id) + + response = Excon::Response.new + response.status = 204 + response + end + end + end + end +end diff --git a/lib/fog/aws/requests/efs/describe_file_systems.rb b/lib/fog/aws/requests/efs/describe_file_systems.rb new file mode 100644 index 0000000000..96f3e39c1a --- /dev/null +++ b/lib/fog/aws/requests/efs/describe_file_systems.rb @@ -0,0 +1,62 @@ +module Fog + module AWS + class EFS + class Real + # Describe all or specified elastic file systems + # http://docs.aws.amazon.com/efs/latest/ug/API_DescribeFileSystems.html + # ==== Parameters + # * CreationToken <~String> - (Optional) Restricts the list to the file system with this creation token (String). You specify a creation token when you create an Amazon EFS file system. + # * FileSystemId <~String> - (Optional) ID of the file system whose description you want to retrieve (String). + # ==== Returns + # * response<~Excon::Response>: + # * body<~Hash> + def describe_file_systems(options={}) + params = {} + if options[:marker] + params['Marker'] = options[:marker] + end + if options[:max_records] + params['MaxRecords'] = options[:max_records] + end + if options[:id] + params['FileSystemId'] = options[:id] + end + if options[:creation_token] + params['CreationToken'] = options[:creation_token] + end + + request({ + :path => "file-systems" + }.merge(params)) + end + end + + class Mock + def describe_file_systems(options={}) + response = Excon::Response.new + + file_systems = if id = options[:id] + if fs = self.data[:file_systems][id] + [fs] + else + raise Fog::AWS::EFS::NotFound.new("invalid file system ID: #{id}") + end + elsif creation_token = options[:creation_token] + fs = self.data[:file_systems].values.detect { |file_system| file_system["CreationToken"] == creation_token } + [fs] + else + self.data[:file_systems].values + end + + file_systems.each do |file_system| + file_system['LifeCycleState'] = 'available' + self.data[:file_systems][file_system['FileSystemId']] = file_system + end + + response.body = {"FileSystems" => file_systems} + response + end + end + end + end +end diff --git a/tests/models/efs/file_system_tests.rb b/tests/models/efs/file_system_tests.rb new file mode 100644 index 0000000000..50f1f0a012 --- /dev/null +++ b/tests/models/efs/file_system_tests.rb @@ -0,0 +1,12 @@ +Shindo.tests("AWS::EFS | file system", ["aws", "efs"]) do + file_system_params = { + :creation_token => "fogtoken#{rand(999).to_s}" + } + + model_tests(Fog::AWS[:efs].file_systems, file_system_params, true) + + file_system_params = { + :creation_token => "fogtoken#{rand(999).to_s}" + } + collection_tests(Fog::AWS[:efs].file_systems, file_system_params, true) +end diff --git a/tests/requests/efs/file_system_tests.rb b/tests/requests/efs/file_system_tests.rb new file mode 100644 index 0000000000..3945ab3aa2 --- /dev/null +++ b/tests/requests/efs/file_system_tests.rb @@ -0,0 +1,36 @@ +Shindo.tests('AWS::EFS | file systems', ['aws', 'efs']) do + suffix = rand(65535).to_s(16) + + @creation_token = "fogtest#{suffix}" + + tests('success') do + tests("#create_file_system").formats(AWS::EFS::Formats::FILE_SYSTEM_FORMAT) do + result = Fog::AWS[:efs].create_file_system(:creation_token => @creation_token).body + returns('creating') { result['LifeCycleState'] } + result + end + + tests("#describe_file_systems").formats(AWS::EFS::Formats::DESCRIBE_FILE_SYSTEMS_RESULT) do + Fog::AWS[:efs].describe_file_systems.body + end + + tests("#describe_file_systems(creation_token: #{@creation_token})").formats(AWS::EFS::Formats::DESCRIBE_FILE_SYSTEMS_RESULT) do + result = Fog::AWS[:efs].describe_file_systems(:creation_token => @creation_token).body + returns(@creation_token) { result["FileSystems"].first["CreationToken"] } + result + end + + file_system = Fog::AWS[:efs].describe_file_systems(:creation_token => @creation_token).body["FileSystems"].first + + tests("#describe_file_systems(id: #{file_system["FileSystemId"]})").formats(AWS::EFS::Formats::DESCRIBE_FILE_SYSTEMS_RESULT) do + Fog::AWS[:efs].describe_file_systems(:id => file_system["FileSystemId"]).body + end + + tests("#delete_file_system") do + returns(true) do + result = Fog::AWS[:efs].delete_file_system(:id => file_system["FileSystemId"]) + result.body.empty? + end + end + end +end diff --git a/tests/requests/efs/helper.rb b/tests/requests/efs/helper.rb new file mode 100644 index 0000000000..8ae0efb4a6 --- /dev/null +++ b/tests/requests/efs/helper.rb @@ -0,0 +1,24 @@ +class AWS + module EFS + module Formats + FILE_SYSTEM_FORMAT = { + "CreationTime" => Float, + "CreationToken" => String, + "FileSystemId" => String, + "LifeCycleState" => String, + "Name" => Fog::Nullable::String, + "NumberOfMountTargets" => Integer, + "OwnerId" => String, + "PerformanceMode" => String, + "SizeInBytes" => { + "Timestamp" => Fog::Nullable::Float, + "Value" => Integer + } + } + + DESCRIBE_FILE_SYSTEMS_RESULT = { + "FileSystems" => [FILE_SYSTEM_FORMAT] + } + end + end +end