You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@deltacloud.apache.org by mf...@redhat.com on 2011/01/13 14:05:53 UTC

[PATCH core] Initial run-on-instance support for EC2 and GoGrid

From: Michal Fojtik <mf...@redhat.com>

---
 server/deltacloud.rb                               |    1 +
 server/lib/deltacloud/drivers/ec2/ec2_driver.rb    |   16 ++-
 .../lib/deltacloud/drivers/gogrid/gogrid_driver.rb |   15 ++
 server/lib/deltacloud/runner.rb                    |  155 ++++++++++++++++++++
 server/server.rb                                   |   23 +++
 server/views/instances/index.html.haml             |    2 +
 server/views/instances/run.html.haml               |    9 +
 server/views/instances/run.xml.haml                |    7 +
 server/views/instances/run_command.html.haml       |   16 ++
 9 files changed, 243 insertions(+), 1 deletions(-)
 create mode 100644 server/lib/deltacloud/runner.rb
 create mode 100644 server/views/instances/run.html.haml
 create mode 100644 server/views/instances/run.xml.haml
 create mode 100644 server/views/instances/run_command.html.haml

diff --git a/server/deltacloud.rb b/server/deltacloud.rb
index 516963e..83f7cfb 100644
--- a/server/deltacloud.rb
+++ b/server/deltacloud.rb
@@ -38,3 +38,4 @@ require 'deltacloud/models/load_balancer'
 
 require 'deltacloud/validation'
 require 'deltacloud/helpers'
+require 'deltacloud/runner'
diff --git a/server/lib/deltacloud/drivers/ec2/ec2_driver.rb b/server/lib/deltacloud/drivers/ec2/ec2_driver.rb
index 7a4b394..5533c73 100644
--- a/server/lib/deltacloud/drivers/ec2/ec2_driver.rb
+++ b/server/lib/deltacloud/drivers/ec2/ec2_driver.rb
@@ -200,6 +200,20 @@ module Deltacloud
             new_instance
           end
         end
+
+        def run_on_instance(credentials, opts={})
+          target = instance(credentials, :id => opts[:id])
+          param = {}
+          param[:credentials] = {
+            :username => 'root', # Default for EC2 Linux instances
+          }
+          param[:port] = opts[:port] || '22'
+          param[:ip] = target.public_addresses
+          param[:private_key] = (opts[:private_key].length > 1) ? opts[:private_key] : nil
+          safely do
+            Deltacloud::Runner.execute(opts[:cmd], param)
+          end
+        end
     
         def reboot_instance(credentials, instance_id)
           ec2 = new_client(credentials)
@@ -686,7 +700,7 @@ module Deltacloud
           {
             :auth => [], # [ ::Aws::AuthFailure ],
             :error => [ ::Aws::AwsError ],
-            :glob => [ /AWS::(\w+)/ ]
+            :glob => [ /AWS::(\w+)/, /Deltacloud::Runner::(\w+)/ ]
           }
         end
 
diff --git a/server/lib/deltacloud/drivers/gogrid/gogrid_driver.rb b/server/lib/deltacloud/drivers/gogrid/gogrid_driver.rb
index 8b9bf9d..909fade 100644
--- a/server/lib/deltacloud/drivers/gogrid/gogrid_driver.rb
+++ b/server/lib/deltacloud/drivers/gogrid/gogrid_driver.rb
@@ -121,6 +121,21 @@ class GogridDriver < Deltacloud::BaseDriver
     end
   end
 
+  def run_on_instance(credentials, opts={})
+    target = instance(credentials, opts[:id])
+    param = {}
+    param[:credentials] = {
+      :username => target.username,
+      :password => target.password,
+    }
+    param[:credentials].merge!({ :password => opts[:password]}) if opts[:password].length>0
+    param[:port] = opts[:port] || '22'
+    param[:ip] = target.public_addresses
+    Deltacloud::Runner.execute(opts[:cmd], param)
+  end
+
+
+
   def list_instances(credentials, id)
     instances = []
     safely do
diff --git a/server/lib/deltacloud/runner.rb b/server/lib/deltacloud/runner.rb
new file mode 100644
index 0000000..52eed40
--- /dev/null
+++ b/server/lib/deltacloud/runner.rb
@@ -0,0 +1,155 @@
+# Copyright (C) 2009, 2010  Red Hat, Inc.
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.  The
+# ASF licenses this file to you 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 'net/ssh'
+require 'socket'
+require 'tempfile'
+
+module Deltacloud
+
+  module Runner
+
+    class RunnerError < StandardError
+      attr_reader :message
+      def initialize(message)
+        @message = message
+        super
+      end
+    end
+    
+    class InstancePortError < RunnerError; end
+    class InstanceNetworkError < RunnerError; end
+    class InstanceSSHError < RunnerError; end
+
+    def self.execute(command, opts={})
+      
+      # First check networking and firewalling
+      network = Network::new(opts[:ip], opts[:port])
+      raise InstanceNetworkError.new unless network.ready?
+      raise InstancePortError.new unless network.port_open?
+
+      # Then check SSH availability
+      
+      ssh = SSH::new(network, opts[:credentials], opts[:private_key])
+
+      # Finaly execute SSH command on instance
+      ssh.execute(command)
+    end
+
+    class Network
+      attr_accessor :ip, :port
+
+      def initialize(ip, port)
+        @ip, @port = ip, port
+      end
+
+      def ready?
+        begin
+          s = TCPSocket.new(@ip, @port)
+          s.close
+        rescue Errno::EHOSTUNREACH
+          return false
+        rescue
+          return true
+        end
+        true
+      end
+
+      def port_open?
+        begin
+          Timeout::timeout(5) do
+            begin
+              s = TCPSocket.new(@ip, @port)
+              s.close
+              return true
+            rescue Errno::ECONNREFUSED, SocketError
+              return false
+            end
+          end
+        rescue Timeout::Error
+        end
+        return false
+      end
+    end
+
+    class SSH
+
+      attr_reader :network
+      attr_accessor :credentials, :key
+      attr_reader :command
+
+      def initialize(network, credentials, key=nil)
+        @network, @credentials, @key = network, credentials, key
+        @result = ""
+      end
+
+      def execute(command)
+        @command = command
+        config = ssh_config(@network, @credentials, @key)
+        begin
+          Net::SSH.start(@network.ip, 'root', config) do |session|
+            session.open_channel do |channel|
+              channel.on_data do |ch, data|
+                @result += data
+              end
+              channel.exec(command)
+              session.loop
+            end
+          end
+        rescue Exception => e
+          raise InstanceSSHError.new("#{e.class.name}: #{e.message}")
+        ensure
+          # FileUtils.rm(config[:keys].first) rescue nil
+        end
+        Deltacloud::Runner::Response.new(self, @result)
+      end
+
+      private
+
+      def ssh_config(network, credentials, key)
+        config = { :port => network.port }
+        config.merge!({ :password => credentials[:password ]}) if credentials[:password]
+        config.merge!({ :keys => [ keyfile(key) ] }) unless key.nil?
+        config
+      end
+
+      # Right now there is no way howto pass private_key using String
+      # eg. without saving key to temporary file.
+      def keyfile(key)
+        keyfile = Tempfile.new("ec2_private.key")
+        key_material = ""
+        key.split("\n").each { |line| key_material+="#{line.strip}\n" if line.strip.size>0 }
+        keyfile.write(key_material) && keyfile.close
+        puts "[*] Using #{keyfile.path} as private key"
+        keyfile.path
+      end
+
+    end
+
+    class Response
+      
+      attr_reader :body
+      attr_reader :ssh
+
+      def initialize(ssh, response_body)
+        @body, @ssh = response_body, ssh
+      end
+
+    end
+
+  end
+end
diff --git a/server/server.rb b/server/server.rb
index 8c3b72c..c26db48 100644
--- a/server/server.rb
+++ b/server/server.rb
@@ -211,6 +211,13 @@ get "/api/instances/new" do
   end
 end
 
+get '/api/instances/:id/run' do
+  @instance = driver.instance(credentials, :id => params[:id])
+  respond_to do |format|
+    format.html { haml :"instances/run_command" }
+  end
+end
+
 get '/api/load_balancers/new' do
   @realms = driver.realms(credentials)
   @instances = driver.instances(credentials) if driver_has_feature?(:register_instance, :load_balancers)
@@ -356,6 +363,22 @@ END
     param :id,           :string, :required
     control { instance_action(:destroy) }
   end
+
+  operation :run, :method => :post, :member => true do
+    description "Run command on instance"
+    with_capability :run_on_instance
+    param :id,          :string,  :required
+    param :cmd,         :string,  :required
+    param :private_key, :string,  :optional
+    param :password,    :string,  :optional
+    control do
+      @output = driver.run_on_instance(credentials, params)
+      respond_to do |format|
+        format.xml { haml :"instances/run" }
+        format.html { haml :"instances/run" }
+      end
+    end
+  end
 end
 
 collection :hardware_profiles do
diff --git a/server/views/instances/index.html.haml b/server/views/instances/index.html.haml
index 2bd4607..e855439 100644
--- a/server/views/instances/index.html.haml
+++ b/server/views/instances/index.html.haml
@@ -28,3 +28,5 @@
         %td
           -instance.actions.each do |action|
             =link_to_action action, self.send(:"#{action}_instance_url", instance.id), instance_action_method(action)
+          - if driver.respond_to?(:run_on_instance) and instance.state=="RUNNING"
+            =link_to_action "Run command", url_for("/api/instances/#{instance.id}/run"), :get
diff --git a/server/views/instances/run.html.haml b/server/views/instances/run.html.haml
new file mode 100644
index 0000000..88c91ee
--- /dev/null
+++ b/server/views/instances/run.html.haml
@@ -0,0 +1,9 @@
+%h1 Run command on instance #{params[:id]}
+
+%p
+  %label Command:
+  %em #{@output.ssh.command}
+%p
+  %strong Command output
+%pre
+  =@output.body
diff --git a/server/views/instances/run.xml.haml b/server/views/instances/run.xml.haml
new file mode 100644
index 0000000..4201e20
--- /dev/null
+++ b/server/views/instances/run.xml.haml
@@ -0,0 +1,7 @@
+%instance{:id => params[:id], :href=> instance_url(params[:id])}
+  %public_address
+    =@output.ssh.network.ip
+  %command
+    =@output.ssh.command
+  %output<
+    =@output.body
diff --git a/server/views/instances/run_command.html.haml b/server/views/instances/run_command.html.haml
new file mode 100644
index 0000000..68e5046
--- /dev/null
+++ b/server/views/instances/run_command.html.haml
@@ -0,0 +1,16 @@
+%h1
+  Run command on 
+  = @instance.id
+
+%form{ :action => run_instance_url(@instance.id), :method => :post }
+  %p
+    %label{ :for => :cmd } Desired command:
+    %input{ :name => :cmd, :value => "", :type => :text}
+  %p
+    %label{ :for => :private_key } Private key:
+  %p
+    %small Leave private key blank if using password authentication method
+  %p
+    %textarea{ :name => :private_key, :cols => 65, :rows => 20 }
+  %p
+    %input{ :type => :submit, :value => "Execute" }
-- 
1.7.3.4


Re: [PATCH core] Initial run-on-instance support for EC2 and GoGrid

Posted by Michal Fojtik <mf...@redhat.com>.
On 14/01/11 17:04 -0800, David Lutterkort wrote:
>Some comments:
>
>On Thu, 2011-01-13 at 14:05 +0100, mfojtik@redhat.com wrote:
>> diff --git a/server/lib/deltacloud/drivers/ec2/ec2_driver.rb b/server/lib/deltacloud/drivers/ec2/ec2_driver.rb
>> index 7a4b394..5533c73 100644
>> --- a/server/lib/deltacloud/drivers/ec2/ec2_driver.rb
>> +++ b/server/lib/deltacloud/drivers/ec2/ec2_driver.rb
>> @@ -200,6 +200,20 @@ module Deltacloud
>>              new_instance
>>            end
>>          end
>> +
>> +        def run_on_instance(credentials, opts={})
>> +          target = instance(credentials, :id => opts[:id])
>
>How about allowing passing the IP to connect to in explicitly ? That
>way, users can avoid this lookup, in case they know the IP already.

Yes, that's right but users will execute commands on specific instance, the
URI for executing commands is: /api/instances/1234/run

Another thing is that the user must know key name in order to execute something
(like in Amazon) (except case that user have just one key for all instances).

I though that we want to fully automate this process (getting an IP from
instance) and ask user just for additional credentials (like private key).

An option could be to add a param for an IP, so user can change it in
request.

>> diff --git a/server/lib/deltacloud/runner.rb b/server/lib/deltacloud/runner.rb
>> new file mode 100644
>> index 0000000..52eed40
>> --- /dev/null
>> +++ b/server/lib/deltacloud/runner.rb
>> @@ -0,0 +1,155 @@
>> +# Copyright (C) 2009, 2010  Red Hat, Inc.
>> +#
>> +# Licensed to the Apache Software Foundation (ASF) under one or more
>> +# contributor license agreements.  See the NOTICE file distributed with
>> +# this work for additional information regarding copyright ownership.  The
>> +# ASF licenses this file to you 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 'net/ssh'
>> +require 'socket'
>> +require 'tempfile'
>> +
>> +module Deltacloud
>> +
>> +  module Runner
>> +
>> +    class RunnerError < StandardError
>> +      attr_reader :message
>> +      def initialize(message)
>> +        @message = message
>> +        super
>> +      end
>> +    end
>> +
>> +    class InstancePortError < RunnerError; end
>> +    class InstanceNetworkError < RunnerError; end
>> +    class InstanceSSHError < RunnerError; end
>> +
>> +    def self.execute(command, opts={})
>
>Shouldn't there be some sanity checking of opts, e.g. that either a
>password or private key is present ?

Yes, I can add a check if key or password is present in opts. 

>> +      # First check networking and firewalling
>> +      network = Network::new(opts[:ip], opts[:port])
>> +      raise InstanceNetworkError.new unless network.ready?
>> +      raise InstancePortError.new unless network.port_open?
>
>I don't like this whole network checking business - you are opening two
>TCP connections, just to check if establishing the SSH connection
>afterwards might work. For one, it's very wasteful timewise; in
>addition, there's no guarantee that the success of the two operations
>above has any connection (pun intended) to whether the SSH conn will
>succeed.
>
>You really need to handle timeouts, conn refused etc. when you establish
>the ssh connection.

You're right, I checked Net:SSH documentation and there are some Exceptions
which we can use for error reporting.

Also I can wrap Net:SSH with timeout block and then catch exception here.

>> diff --git a/server/server.rb b/server/server.rb
>> index 8c3b72c..c26db48 100644
>> --- a/server/server.rb
>> +++ b/server/server.rb
>> @@ -356,6 +363,22 @@ END
>>      param :id,           :string, :required
>>      control { instance_action(:destroy) }
>>    end
>> +
>> +  operation :run, :method => :post, :member => true do
>> +    description "Run command on instance"
>> +    with_capability :run_on_instance
>> +    param :id,          :string,  :required
>> +    param :cmd,         :string,  :required
>> +    param :private_key, :string,  :optional
>> +    param :password,    :string,  :optional
>
>The params need documentation, especially the fact that either
>private_key or password need to be given. Also, how will clients know
>which one to pass ?

Yes, I'll add a description param here.

Client can query instance authentication method to know what sort of
credentials are needed for successful authentication.
I'll mention that in description of this operation.

-- Michal

-- 
--------------------------------------------------------
Michal Fojtik, mfojtik@redhat.com
Deltacloud API: http://deltacloud.org
--------------------------------------------------------

Re: [PATCH core] Initial run-on-instance support for EC2 and GoGrid

Posted by David Lutterkort <lu...@redhat.com>.
Some comments:

On Thu, 2011-01-13 at 14:05 +0100, mfojtik@redhat.com wrote:
> diff --git a/server/lib/deltacloud/drivers/ec2/ec2_driver.rb b/server/lib/deltacloud/drivers/ec2/ec2_driver.rb
> index 7a4b394..5533c73 100644
> --- a/server/lib/deltacloud/drivers/ec2/ec2_driver.rb
> +++ b/server/lib/deltacloud/drivers/ec2/ec2_driver.rb
> @@ -200,6 +200,20 @@ module Deltacloud
>              new_instance
>            end
>          end
> +
> +        def run_on_instance(credentials, opts={})
> +          target = instance(credentials, :id => opts[:id])

How about allowing passing the IP to connect to in explicitly ? That
way, users can avoid this lookup, in case they know the IP already.

> diff --git a/server/lib/deltacloud/runner.rb b/server/lib/deltacloud/runner.rb
> new file mode 100644
> index 0000000..52eed40
> --- /dev/null
> +++ b/server/lib/deltacloud/runner.rb
> @@ -0,0 +1,155 @@
> +# Copyright (C) 2009, 2010  Red Hat, Inc.
> +#
> +# Licensed to the Apache Software Foundation (ASF) under one or more
> +# contributor license agreements.  See the NOTICE file distributed with
> +# this work for additional information regarding copyright ownership.  The
> +# ASF licenses this file to you 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 'net/ssh'
> +require 'socket'
> +require 'tempfile'
> +
> +module Deltacloud
> +
> +  module Runner
> +
> +    class RunnerError < StandardError
> +      attr_reader :message
> +      def initialize(message)
> +        @message = message
> +        super
> +      end
> +    end
> +    
> +    class InstancePortError < RunnerError; end
> +    class InstanceNetworkError < RunnerError; end
> +    class InstanceSSHError < RunnerError; end
> +
> +    def self.execute(command, opts={})

Shouldn't there be some sanity checking of opts, e.g. that either a
password or private key is present ?

> +      # First check networking and firewalling
> +      network = Network::new(opts[:ip], opts[:port])
> +      raise InstanceNetworkError.new unless network.ready?
> +      raise InstancePortError.new unless network.port_open?

I don't like this whole network checking business - you are opening two
TCP connections, just to check if establishing the SSH connection
afterwards might work. For one, it's very wasteful timewise; in
addition, there's no guarantee that the success of the two operations
above has any connection (pun intended) to whether the SSH conn will
succeed.

You really need to handle timeouts, conn refused etc. when you establish
the ssh connection.

> diff --git a/server/server.rb b/server/server.rb
> index 8c3b72c..c26db48 100644
> --- a/server/server.rb
> +++ b/server/server.rb
> @@ -356,6 +363,22 @@ END
>      param :id,           :string, :required
>      control { instance_action(:destroy) }
>    end
> +
> +  operation :run, :method => :post, :member => true do
> +    description "Run command on instance"
> +    with_capability :run_on_instance
> +    param :id,          :string,  :required
> +    param :cmd,         :string,  :required
> +    param :private_key, :string,  :optional
> +    param :password,    :string,  :optional

The params need documentation, especially the fact that either
private_key or password need to be given. Also, how will clients know
which one to pass ? 

David