incubator-deltacloud-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From mfoj...@redhat.com
Subject [PATCH core] Initial run-on-instance support for EC2 and GoGrid
Date Thu, 13 Jan 2011 13:05:53 GMT
From: Michal Fojtik <mfojtik@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


Mime
View raw message