incubator-deltacloud-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From Michal Fojtik <mfoj...@redhat.com>
Subject Re: [PATCH core] Initial run-on-instance support for EC2 and GoGrid
Date Thu, 27 Jan 2011 13:04:06 GMT
On 27/01/11 13:53 +0100, Michal Fojtik wrote:
>On 26/01/11 14:25 -0500, Toby Crawley wrote:
>>Michal:
>>
>>I tested this from the http ui, against ec2, and it works fine for me. I don't have
a gogrid account set up, so I did not test that.
>>
>>I'd like to test it from the client, but don't see an action on the instance to run
a command. Is there currently a way to do it from the client? If I can get it working from
the client, I'll ACK.

Btw. naughty way howto install a missing gem on our EC2 Hudson:

irb> puts client.instance('i-e90ead85').run('gem install net-ssh', :pem => '/home/mfojtik/Downloads/hudson.pem')
Successfully installed net-ssh-2.1.0
1 gem installed

;-) Which remind me: you need net-ssh gem installed in order to execute
commands on instances.

   -- Michal

>
>Yes, client was missing support for this feature, however I implemented it
>to client and now you can do this magic:
>
>client = DeltaCloud::new(user, pass, url)
>puts client.instance('i-1234565').run('uname -a', :pem => path_to_pem_file)
>
>Note: I added 'run' link to instance in EC2 to instruct client library to
>pick it up and wrap with run logic.
>
>Pushed to master.
>
>  -- Michal
>
>>Toby
>>
>>On Jan 17, 2011, at 6:28 AM, mfojtik@redhat.com wrote:
>>
>>>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                    |  129 ++++++++++++++++++++
>>>server/lib/deltacloud/validation.rb                |    8 +-
>>>server/server.rb                                   |   27 ++++
>>>server/views/docs/operation.html.haml              |    2 +
>>>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 +++
>>>11 files changed, 229 insertions(+), 3 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..a4e5e25
>>>--- /dev/null
>>>+++ b/server/lib/deltacloud/runner.rb
>>>@@ -0,0 +1,129 @@
>>>+# 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 InstanceSSHError < RunnerError; end
>>>+
>>>+    def self.execute(command, opts={})
>>>+
>>>+      if opts[:credentials] and (not opts[:credentials][:password] and not opts[:private_key])
>>>+        raise RunnerError::new("Either password or key must be specified")
>>>+      end
>>>+
>>>+      # First check networking and firewalling
>>>+      network = Network::new(opts[:ip], opts[:port])
>>>+
>>>+      # 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
>>>+    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
>>>+          session = nil
>>>+          Timeout::timeout(5) do
>>>+            session = Net::SSH.start(@network.ip, 'root', config)
>>>+          end
>>>+          session.open_channel do |channel|
>>>+            channel.on_data do |ch, data|
>>>+              @result += data
>>>+            end
>>>+            channel.exec(command)
>>>+            session.loop
>>>+          end
>>>+          session.close
>>>+        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/lib/deltacloud/validation.rb b/server/lib/deltacloud/validation.rb
>>>index 18e71cc..e3ca911 100644
>>>--- a/server/lib/deltacloud/validation.rb
>>>+++ b/server/lib/deltacloud/validation.rb
>>>@@ -37,8 +37,12 @@ module Deltacloud::Validation
>>>      @name = args[0]
>>>      @klass = args[1] || :string
>>>      @type = args[2] || :optional
>>>-      @options = args[3] || []
>>>-      @description = args[4] || ''
>>>+      if args[3] and args[3].class.eql?(String)
>>>+        @description = args[3]
>>>+        @options = []
>>>+      end
>>>+      @options ||= args[3] || []
>>>+      @description ||= args[4] || ''
>>>    end
>>>
>>>    def required?
>>>diff --git a/server/server.rb b/server/server.rb
>>>index 8c3b72c..bbcf6c6 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,26 @@ END
>>>    param :id,           :string, :required
>>>    control { instance_action(:destroy) }
>>>  end
>>>+
>>>+  operation :run, :method => :post, :member => true do
>>>+    description <<END
>>>+  Run command on instance. Either password or private key must be send
>>>+  in order to execute command. Authetication method should be advertised
>>>+  in instance.
>>>+END
>>>+    with_capability :run_on_instance
>>>+    param :id,          :string,  :required
>>>+    param :cmd,         :string,  :required, "Shell command to run on instance"
>>>+    param :private_key, :string,  :optional, "Private key in PEM format for authentication"
>>>+    param :password,    :string,  :optional, "Password used for authentication"
>>>+    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/docs/operation.html.haml b/server/views/docs/operation.html.haml
>>>index 4b483b8..b7c3538 100644
>>>--- a/server/views/docs/operation.html.haml
>>>+++ b/server/views/docs/operation.html.haml
>>>@@ -17,6 +17,7 @@
>>>      %th Type
>>>      %th Class
>>>      %th Valid values
>>>+      %th Description
>>>  %tbody
>>>    - @operation.each_param do |p|
>>>      %tr
>>>@@ -25,3 +26,4 @@
>>>        %td{:style => "width:10em"} #{p.type}
>>>        %td #{p.klass}
>>>        %td{:style => "width:10em"} #{p.options.join(',')}
>>>+        %td #{p.description}
>>>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
>>>
>>
>>---
>>Toby Crawley
>>tcrawley@redhat.com
>>
>>
>>
>>
>
>-- 
>--------------------------------------------------------
>Michal Fojtik, mfojtik@redhat.com
>Deltacloud API: http://deltacloud.org
>--------------------------------------------------------

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

Mime
View raw message