From: Michal Fojtik <[email protected]>
---
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
+ [email protected]
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
+ [email protected]
+ %command
+ [email protected]
+ %output<
+ [email protected]
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