diff --git a/Gemfile.lock b/Gemfile.lock index 00cbd8c33b..9ca604ed9a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,8 +7,8 @@ PATH formatador (>= 0.0.15) json mime-types - net-ssh - nokogiri (>= 1.4.3.1) + net-ssh (~> 2.0.23) + nokogiri (~> 1.4.3.1) ruby-hmac GEM @@ -40,8 +40,8 @@ DEPENDENCIES formatador (>= 0.0.15) json mime-types - net-ssh - nokogiri (>= 1.4.3.1) + net-ssh (~> 2.0.23) + nokogiri (~> 1.4.3.1) rake rspec ruby-hmac diff --git a/fog.gemspec b/fog.gemspec index 71963a6698..4107774dc0 100644 --- a/fog.gemspec +++ b/fog.gemspec @@ -47,8 +47,8 @@ Gem::Specification.new do |s| s.add_dependency('formatador', '>=0.0.15') s.add_dependency('json') s.add_dependency('mime-types') - s.add_dependency('net-ssh') - s.add_dependency('nokogiri', '>=1.4.3.1') + s.add_dependency('net-ssh', '~>2.0.23') + s.add_dependency('nokogiri', '~>1.4.3.1') s.add_dependency('ruby-hmac') ## List your development dependencies here. Development dependencies are diff --git a/lib/fog.rb b/lib/fog.rb index 4eb9f939a1..1a26de44e2 100644 --- a/lib/fog.rb +++ b/lib/fog.rb @@ -8,6 +8,7 @@ require 'mime/types' require 'net/ssh' require 'nokogiri' +require 'tempfile' require 'time' __DIR__ = File.dirname(__FILE__) diff --git a/lib/fog/aws/models/compute/security_group.rb b/lib/fog/aws/models/compute/security_group.rb index 9050ccbd25..03c358a758 100644 --- a/lib/fog/aws/models/compute/security_group.rb +++ b/lib/fog/aws/models/compute/security_group.rb @@ -30,7 +30,7 @@ def authorize_port_range(range, options = {}) 'FromPort' => range.min, 'GroupName' => @name, 'ToPort' => range.max, - 'IpProtocol' => options[:ip_protocol] || 'tcp' + 'IpProtocol' => options[:ip_protocol] || 'tcp' ) end @@ -41,6 +41,28 @@ def destroy true end + def revoke_group_and_owner(group, owner) + requires :name + + connection.revoke_security_group_ingress( + 'GroupName' => @name, + 'SourceSecurityGroupName' => group, + 'SourceSecurityGroupOwnerId' => owner + ) + end + + def revoke_port_range(range, options = {}) + requires :name + + connection.revoke_security_group_ingress( + 'CidrIp' => options[:cidr_ip] || '0.0.0.0/0', + 'FromPort' => range.min, + 'GroupName' => @name, + 'ToPort' => range.max, + 'IpProtocol' => options[:ip_protocol] || 'tcp' + ) + end + def save requires :description, :name diff --git a/lib/fog/aws/models/compute/server.rb b/lib/fog/aws/models/compute/server.rb index d49b18b11b..d551e83f9e 100644 --- a/lib/fog/aws/models/compute/server.rb +++ b/lib/fog/aws/models/compute/server.rb @@ -32,6 +32,8 @@ class Server < Fog::Model attribute :subnet_id, :aliases => 'subnetId' attribute :user_data + attr_accessor :password, :private_key_path, :public_key_path, :username + def initialize(attributes={}) @groups ||= ["default"] unless attributes[:subnet_id] @flavor_id ||= 'm1.small' @@ -57,14 +59,6 @@ def destroy true end - # def security_group - # connection.security_groups.all(@group_id) - # end - # - # def security_group=(new_security_group) - # @group_id = new_security_group.name - # end - def flavor_id @flavor && @flavor.id || @flavor_id end @@ -103,6 +97,14 @@ def placement=(new_placement) end end + def private_key_path + @private_key_path ||= Fog.credentials[:private_key_path] + end + + def public_key_path + @public_key_path ||= Fog.credentials[:public_key_path] + end + def ready? @state == 'running' end @@ -117,18 +119,18 @@ def save requires :image_id options = { - 'BlockDeviceMapping' => @block_device_mapping, + 'BlockDeviceMapping' => block_device_mapping, 'InstanceType' => flavor_id, - 'KernelId' => @kernel_id, - 'KeyName' => @key_name, - 'Monitoring.Enabled' => @monitoring, - 'Placement.AvailabilityZone' => @availability_zone, - 'RamdiskId' => @ramdisk_id, - 'SecurityGroup' => @groups, + 'KernelId' => kernel_id, + 'KeyName' => key_name, + 'Monitoring.Enabled' => monitoring, + 'Placement.AvailabilityZone' => availability_zone, + 'RamdiskId' => ramdisk_id, + 'SecurityGroup' => groups, 'SubnetId' => subnet_id, - 'UserData' => @user_data + 'UserData' => user_data } - + # If subnet is defined we are working on a virtual private cloud. # subnet & security group cannot co-exist. I wish VPC just ignored # the security group parameter instead, it would be much easier! @@ -138,11 +140,31 @@ def save options.delete('SubnetId') end - data = connection.run_instances(@image_id, 1, 1, options) + data = connection.run_instances(image_id, 1, 1, options) merge_attributes(data.body['instancesSet'].first) true end + def setup(credentials = {}) + requires :ip_address, :identity, :public_key_path, :username + sleep(10) # takes a bit before EC2 instances will play nice + Fog::SSH.new(ip_address, username, credentials).run([ + %{mkdir .ssh}, + %{echo "#{File.read(File.expand_path(public_key_path))}" >> ~/.ssh/authorized_keys}, + %{passwd -l root}, + %{echo "#{attributes.to_json}" >> ~/attributes.json} + ]) + rescue Errno::ECONNREFUSED => e + sleep(1) + retry + end + + def ssh(commands) + requires :identity, :ip_address, :private_key_path, :username + @ssh ||= Fog::SSH.new(ip_address, username, :keys => [private_key_path]) + @ssh.run(commands) + end + def start requires :id connection.start_instances(@id) @@ -155,6 +177,10 @@ def stop true end + def username + @username ||= 'root' + end + def volumes requires :id diff --git a/lib/fog/aws/models/compute/servers.rb b/lib/fog/aws/models/compute/servers.rb index 98b9b41c97..abed1a13df 100644 --- a/lib/fog/aws/models/compute/servers.rb +++ b/lib/fog/aws/models/compute/servers.rb @@ -28,6 +28,33 @@ def all(server_id = @server_id) ) end + def bootstrap(new_attributes = {}) + begin + tmp_key_pair = connection.key_pairs.create(:name => "tmp_#{Time.now.to_f.to_s.gsub('.','')}") + server = create(new_attributes.merge(:key_pair => tmp_key_pair)) + + # make sure port 22 is open in the first security group + security_group = connection.security_groups.get(server.groups.first) + ip_permission = security_group.ip_permissions.detect do |ip_permission| + ip_permission['ipRanges'].first && ip_permission['ipRanges'].first['cidrIp'] == '0.0.0.0/0' && + ip_permission['fromPort'] == 22 && + ip_permission['ipProtocol'] == 'tcp' && + ip_permission['toPort'] == 22 + end + unless ip_permission + security_group.authorize_port_range(22..22) + end + + server.wait_for { ready? } + server.setup(:key_data => [tmp_key_pair.material]) + ensure + if tmp_key_pair + tmp_key_pair.destroy + end + end + server + end + def get(server_id) if server_id all(server_id).first diff --git a/lib/fog/rackspace/models/compute/server.rb b/lib/fog/rackspace/models/compute/server.rb index 61c2f37753..6134ab240b 100644 --- a/lib/fog/rackspace/models/compute/server.rb +++ b/lib/fog/rackspace/models/compute/server.rb @@ -18,7 +18,7 @@ class Server < Fog::Model attribute :progress attribute :status - attr_accessor :password, :private_key_path, :public_key_path + attr_accessor :password, :private_key_path, :public_key_path, :username def initialize(attributes={}) @flavor_id ||= 1 @@ -57,11 +57,11 @@ def reboot(type = 'SOFT') end def private_key_path - @private_key_path || Fog.credentials[:private_key_path] + @private_key_path ||= Fog.credentials[:private_key_path] end def public_key_path - @public_key_path || Fog.credentials[:public_key_path] + @public_key_path ||= Fog.credentials[:public_key_path] end def save @@ -77,15 +77,9 @@ def save true end - def ssh(commands) - requires :addresses, :identity, :private_key_path - @ssh ||= Fog::SSH.new(@addresses['public'].first, 'root', :keys => [private_key_path]) - @ssh.run(commands) - end - - def setup - requires :addresses, :identity, :password, :public_key_path - Fog::SSH.new(@addresses['public'].first, 'root', :password => password).run([ + def setup(credentials = {}) + requires :addresses, :identity, :public_key_path, :username + Fog::SSH.new(addresses['public'].first, username, credentials).run([ %{mkdir .ssh}, %{echo "#{File.read(File.expand_path(public_key_path))}" >> ~/.ssh/authorized_keys}, %{passwd -l root}, @@ -97,6 +91,16 @@ def setup retry end + def ssh(commands) + requires :addresses, :identity, :private_key_path, :username + @ssh ||= Fog::SSH.new(addresses['public'].first, username, :keys => [private_key_path]) + @ssh.run(commands) + end + + def username + @username ||= root + end + private def adminPass=(new_admin_pass) diff --git a/lib/fog/rackspace/models/compute/servers.rb b/lib/fog/rackspace/models/compute/servers.rb index 43d9acfaae..efb03b6431 100644 --- a/lib/fog/rackspace/models/compute/servers.rb +++ b/lib/fog/rackspace/models/compute/servers.rb @@ -17,7 +17,7 @@ def all def bootstrap(new_attributes = {}) server = create(new_attributes) server.wait_for { ready? } - server.setup + server.setup(:password => server.password) server end diff --git a/lib/fog/ssh.rb b/lib/fog/ssh.rb index 8deb10f61e..962cb7db2f 100644 --- a/lib/fog/ssh.rb +++ b/lib/fog/ssh.rb @@ -2,8 +2,8 @@ module Fog module SSH def self.new(address, username, options = {}) - unless options[:keys] || options[:password] - raise ArgumentError.new(':keys or :password are required to initialize SSH') + unless options[:key_data] || options[:keys] || options[:password] + raise ArgumentError.new(':key_data, :keys or :password are required to initialize SSH') end if Fog.mocking? Fog::SSH::Mock.new(address, username, options) @@ -41,7 +41,8 @@ class Real def initialize(address, username, options) @address = address @username = username - @options = options.merge!(:paranoid => false) + @options = options.merge(:paranoid => false) + @options.merge(:verbose => true) end def run(commands) @@ -50,11 +51,11 @@ def run(commands) begin Net::SSH.start(@address, @username, @options) do |ssh| commands.each do |command| + sudoable_command = command.sub(/^sudo/, %{sudo -p 'fog sudo password:'}) + escaped_command = sudoable_command.sub(/'/, %{'"'"'}) + result = Result.new(escaped_command) ssh.open_channel do |channel| - sudoable_command = command.sub(/^sudo/, %{sudo -p 'fog sudo password:'}) - escaped_command = sudoable_command.sub(/'/, %{'"'"'}) channel.request_pty - result = Result.new(escaped_command) channel.exec(%{bash -lc '#{escaped_command}'}) do |channel, success| unless success raise "Could not execute command: #{command.inspect}" @@ -77,9 +78,9 @@ def run(commands) result.status = 255 end end - results << result end ssh.loop + results << result end end rescue Net::SSH::HostKeyMismatch => exception @@ -96,6 +97,14 @@ class Result attr_accessor :command, :stderr, :stdout, :status + def display_stdout + Formatador.display_line(stdout.split("\r\n")) + end + + def display_stderr + Formatador.display_line(stderr.split("\r\n")) + end + def initialize(command) @command = command @stderr = ''