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