diff --git a/README.rdoc b/README.rdoc
index 6502e4b..0363193 100644
--- a/README.rdoc
+++ b/README.rdoc
@@ -22,17 +22,17 @@ This plugin provides the following Knife subcommands. Specific command options
The job list subcommand is used to view a list of Push jobs.
-== Syntax
+=== Syntax
$ knife job list
== job start
The job start subcommand is used to start a Push job.
-== Syntax
+=== Syntax
$ knife job start (options) COMMAND [NODE, NODE, ...]
-== Options
+=== Options
This argument has the following options:
--timeout TIMEOUT
@@ -48,7 +48,26 @@ percentage (e.g. 50%) or as an absolute number of nodes (e.g. 145). Default valu
Exit immediately after starting a job instead of waiting for it to complete.
-== Examples
+ --with-env ENVIRONMENT
+
+Accept a json blob of environment variables and use those to set the
+variables for the client. For example '{"test": "foo"}' will set the
+push client environment variable "test" to "foo". (Push 2.0 and later)
+
+ --in-dir DIR
+
+Execute the remote command in the directory DIR. (Push 2.0 and later)
+
+ --file DATAFILE
+
+Send the file to the client. (Push 2.0 and later)
+
+ --capture
+
+Capture stdin and stdout for this job. (Push 2.0 and later)
+
+
+=== Examples
For example, to search for nodes assigned the role “webapp”, and where 90% of those nodes must be available, enter:
$ knife job start -quorum 90% 'chef-client' --search 'role:webapp'
@@ -65,14 +84,35 @@ For example, to run a job named add-glasses against a node named “ricardosalaz
$ knife job start add-glasses 'ricardosalazar'
+== job output
+
+The job output command is used to view the output of Push
+jobs. (Push 2.0 and later). The output capture flag must have been set
+on job start; see the --capture option.
+
+=== Syntax
+
+ $ knife job output JOBID
+
+=== Examples
+
+ $ knife job output 26e98ba162fa7ba6fb2793125553c7ae test --channel stdout
+
+=== Options
+
+ --channel [stderr|stdout]
+
+The output channel to capture.
+
+
== job status
-The job status argument is used to view the status of Push jobs.
+The job status command is used to view the status of Push jobs.
-== Syntax
- $ knife job status
+=== Syntax
+ $ knife job status JOBID
-== Examples
+=== Examples
For example, to view the status of a job that has the identifier of “235”, enter:
$ knife job status 235
@@ -81,10 +121,12 @@ For example, to view the status of a job that has the identifier of “235”, e
The node status argument is used to identify nodes that Push may interact with.
-== Syntax
+=== Syntax
$ knife node status
+
+
== License
Push - The push jobs component for chef
diff --git a/knife-push.gemspec b/knife-push.gemspec
index beb979b..fb120d6 100644
--- a/knife-push.gemspec
+++ b/knife-push.gemspec
@@ -7,11 +7,11 @@ Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.has_rdoc = true
s.extra_rdoc_files = ["README.rdoc", "LICENSE"]
- s.summary = "Knife plugin for OPC push"
+ s.summary = "Knife plugin for chef push"
s.description = s.summary
s.author = "John Keiser"
s.email = "jkeiser@opscode.com"
- s.homepage = "http://www.opscode.com"
+ s.homepage = "http://www.chef.io"
# We need a more recent version of mixlib-cli in order to support --no- options.
# ... but, we can live with those options not working, if it means the plugin
diff --git a/lib/chef/knife/job_helpers.rb b/lib/chef/knife/job_helpers.rb
new file mode 100644
index 0000000..2765acc
--- /dev/null
+++ b/lib/chef/knife/job_helpers.rb
@@ -0,0 +1,142 @@
+# @copyright Copyright 2015 Chef Software, Inc. All Rights Reserved.
+#
+# This file is provided 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.
+#
+
+class Chef
+ class Knife
+ module JobHelpers
+ def self.process_search(search, nodes)
+ node_names = []
+ if search
+ q = Chef::Search::Query.new
+ escaped_query = URI.escape(search,
+ Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
+ begin
+ nodes = q.search(:node, escaped_query).first
+ rescue Net::HTTPServerException => e
+ msg Chef::JSONCompat.from_json(e.response.body)['error'].first
+ ui.error("knife search failed: #{msg}")
+ exit 1
+ end
+ nodes.each { |node| node_names << node.name }
+ else
+ node_names = nodes
+ end
+
+ if node_names.empty?
+ ui.error "No nodes to run job on. Specify nodes as arguments or use -s to specify a search query."
+ exit 1
+ end
+
+ return node_names
+ end
+
+ def self.status_string(job)
+ case job['status']
+ when 'new'
+ [false, 'Initialized.']
+ when 'voting'
+ [false, job['status'].capitalize + '.']
+ else
+ total = job['nodes'].values.inject(0) { |sum,nodes| sum+nodes.length }
+ in_progress = job['nodes'].keys.inject(0) { |sum,status|
+ nodes = job['nodes'][status]
+ sum + (%w(new voting running).include?(status) ? 1 : 0)
+ }
+ if job['status'] == 'running'
+ [false, job['status'].capitalize + " (#{in_progress}/#{total} in progress) ..."]
+ else
+ [true, job['status'].capitalize + '.']
+ end
+ end
+ end
+
+ def self.get_quorum(quorum, total_nodes)
+ unless qmatch = /^(\d+)(\%?)$/.match(quorum)
+ raise "Invalid Format please enter integer or percent"
+ end
+
+ num = qmatch[1]
+
+ case qmatch[2]
+ when "%" then
+ ((num.to_f/100)*total_nodes).ceil
+ else
+ num.to_i
+ end
+ end
+
+ def self.status_code(job)
+ if job['status'] == "complete" && job["nodes"].keys.all? do |key|
+ key == "succeeded" || key == "nacked" || key == "unavailable"
+ end
+ 0
+ else
+ 1
+ end
+ end
+
+ def self.run_helper(config, job_json)
+ job_json['run_timeout'] ||= config[:run_timeout].to_i if config[:run_timeout]
+
+ rest = Chef::REST.new(Chef::Config[:chef_server_url])
+ result = rest.post_rest('pushy/jobs', job_json)
+ job_uri = result['uri']
+ puts "Started. Job ID: #{job_uri[-32,32]}"
+ exit(0) if config[:nowait]
+ previous_state = "Initialized."
+ begin
+ sleep(config[:poll_interval].to_f)
+ putc(".")
+ job = rest.get_rest(job_uri)
+ finished, state = JobHelpers.status_string(job)
+ if state != previous_state
+ puts state
+ previous_state = state
+ end
+ end until finished
+ job
+ end
+
+ def self.file_helper(file_name)
+ if file_name.nil?
+ ui.error "No file specified."
+ show_usage
+ exit 1
+ end
+ contents = ""
+ if File.exists?(file_name)
+ File.open(file_name, "rb") do |file|
+ contents = file.read
+ end
+ else
+ ui.error "#{file_name} not found"
+ exit 1
+ end
+ return contents
+ end
+
+ def self.get_env(config)
+ env = {}
+ begin
+ env = config[:with_env] ? JSON.parse(config[:with_env]) : {}
+ rescue Exception => e
+ Chef::Log.info("Can't parse environment as JSON")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/job_output.rb b/lib/chef/knife/job_output.rb
new file mode 100644
index 0000000..5f7e412
--- /dev/null
+++ b/lib/chef/knife/job_output.rb
@@ -0,0 +1,57 @@
+# @copyright Copyright 2014 Chef Software, Inc. All Rights Reserved.
+#
+# This file is provided 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.
+#
+
+class Chef
+ class Knife
+ class JobOutput < Chef::Knife
+ banner "knife job output [ ...]"
+
+ option :channel,
+ :long => '--channel stdout|stderr',
+ :default => 'stdout',
+ :description => "Which output channel to fetch (default stdout)."
+
+ def run
+ rest = Chef::REST.new(Chef::Config[:chef_server_url])
+
+ job_id = name_args[0]
+ channel = get_channel(config[:channel])
+ node = name_args[1]
+
+ uri = "pushy/jobs/#{job_id}/output/#{node}/#{channel}"
+
+ job = rest.get_rest(uri, false, {"Accept"=>"application/octet-stream"})
+
+ output(job)
+ end
+
+ def get_channel(channel)
+ channel = channel || "stdout"
+ case channel
+ when "stdout"
+ return channel
+ when "stderr"
+ return channel
+ else
+ raise "Invalid Format please enter stdout or stderr"
+ end
+ end
+
+ end
+ end
+end
+
diff --git a/lib/chef/knife/job_start.rb b/lib/chef/knife/job_start.rb
index 8f42fd9..e1e8dd2 100644
--- a/lib/chef/knife/job_start.rb
+++ b/lib/chef/knife/job_start.rb
@@ -43,21 +43,45 @@ class JobStart < Chef::Knife
:required => false,
:description => 'Solr query for list of job candidates.'
+ option :send_file,
+ :long => '--file FILE',
+ :default => nil,
+ :description => 'File to send to job.'
+
+ option :capture_output,
+ :long => '--capture',
+ :boolean => true,
+ :default => false,
+ :description => 'Capture job output.'
+
+ option :with_env,
+ :long => "--with-env ENV",
+ :default => nil,
+ :description => 'JSON blob of environment variables to set.'
+
+ option :as_user,
+ :long => "--as-user USER",
+ :default => nil,
+ :description => 'User id to run as.'
+
+ option :in_dir,
+ :long => "--in-dir DIR",
+ :default => nil,
+ :description => 'Directory to execute the command in.'
+
option :nowait,
- :long => '--nowait',
- :short => '-b',
- :boolean => true,
- :default => false,
- :description => "Rather than waiting for each job to complete, exit immediately after starting the job."
+ :long => '--nowait',
+ :short => '-b',
+ :boolean => true,
+ :default => false,
+ :description => "Rather than waiting for each job to complete, exit immediately after starting the job."
option :poll_interval,
:long => '--poll-interval RATE',
:default => 1.0,
:description => "Repeat interval for job status update (in seconds)."
-
- def run
- @node_names = []
+ def run
job_name = @name_args[0]
if job_name.nil?
ui.error "No job specified."
@@ -65,102 +89,29 @@ def run
exit 1
end
- if config[:search]
- q = Chef::Search::Query.new
- @escaped_query = URI.escape(config[:search],
- Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
- begin
- nodes = q.search(:node, @escaped_query).first
- rescue Net::HTTPServerException => e
- msg Chef::JSONCompat.from_json(e.response.body)['error'].first
- ui.error("knife search failed: #{msg}")
- exit 1
- end
- nodes.each { |node| @node_names << node.name }
- else
- @node_names = name_args[1,name_args.length-1]
- end
-
- if @node_names.empty?
- ui.error "No nodes to run job on. Specify nodes as arguments or use -s to specify a search query."
- exit 1
- end
-
- rest = Chef::REST.new(Chef::Config[:chef_server_url])
+ @node_names = JobHelpers.process_search(config[:search], name_args[1,@name_args.length-1])
job_json = {
'command' => job_name,
'nodes' => @node_names,
- 'quorum' => get_quorum(config[:quorum], @node_names.length)
+ 'capture_output' => config[:capture_output]
}
- job_json['run_timeout'] = config[:run_timeout].to_i if config[:run_timeout]
- result = rest.post_rest('pushy/jobs', job_json)
- job_uri = result['uri']
- puts "Started. Job ID: #{job_uri[-32,32]}"
- exit(0) if config[:nowait]
- previous_state = "Initialized."
- begin
- sleep(config[:poll_interval].to_f)
- putc(".")
- job = rest.get_rest(job_uri)
- finished, state = status_string(job)
- if state != previous_state
- puts state
- previous_state = state
- end
- end until finished
-
- output(job)
-
- exit(status_code(job))
- end
-
- private
+ job_json['file'] = "raw:" + JobHelpers.file_helper(config[:send_file]) if config[:send_file]
+ job_json['quorum'] = JobHelpers.get_quorum(config[:quorum], @node_names.length)
+ env = JobHelpers.get_env(config)
+ job_json['env'] = env if env
+ job_json['dir'] = config[:in_dir] if config[:in_dir]
+ job_json['user'] = config[:as_user] if config[:as_user]
- def status_string(job)
- case job['status']
- when 'new'
- [false, 'Initialized.']
- when 'voting'
- [false, job['status'].capitalize + '.']
- else
- total = job['nodes'].values.inject(0) { |sum,nodes| sum+nodes.length }
- in_progress = job['nodes'].keys.inject(0) { |sum,status|
- nodes = job['nodes'][status]
- sum + (%w(new voting running).include?(status) ? 1 : 0)
- }
- if job['status'] == 'running'
- [false, job['status'].capitalize + " (#{in_progress}/#{total} in progress) ..."]
- else
- [true, job['status'].capitalize + '.']
- end
- end
- end
+ job = JobHelpers.run_helper(config, job_json)
- def get_quorum(quorum, total_nodes)
- unless qmatch = /^(\d+)(\%?)$/.match(quorum)
- raise "Invalid Format please enter integer or percent"
- end
+ output(job)
- num = qmatch[1]
+ exit(JobHelpers.status_code(job))
- case qmatch[2]
- when "%" then
- ((num.to_f/100)*total_nodes).ceil
- else
- num.to_i
- end
end
- def status_code(job)
- if job['status'] == "complete" && job["nodes"].keys.all? do |key|
- key == "succeeded" || key == "nacked" || key == "unavailable"
- end
- 0
- else
- 1
- end
- end
+ private
end
end
diff --git a/lib/knife-push/version.rb b/lib/knife-push/version.rb
index 536291f..29e0c2d 100644
--- a/lib/knife-push/version.rb
+++ b/lib/knife-push/version.rb
@@ -1,7 +1,7 @@
module Knife
module Push
- VERSION = '0.6.0'
+ VERSION = '0.9.0'
MAJOR, MINOR, TINY = VERSION.split('.')
end
end