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. Toby On Jan 17, 2011, at 6:28 AM, [email protected] wrote: > 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 > --- Toby Crawley [email protected]
