#!/usr/bin/ruby require 'syslog' # # This script will delete or update the values of a particular couchdb document. The benefit of this little script over # using a simple curl command for updating a document is this: # # * exit non-zero status if document was not updated. # * updates existing documents easily, taking care of the _rev id for you. # * if document doesn't exist, it is created # # REQUIREMENTS # # gem 'couchrest' # # USAGE # # see the ouput of # # couch-doc-update # # the content of <file> will be merged with the data provided. # If you only want the file content use --data '{}' # # EXAMPLE # # create a new user: # couch-doc-update --db _users --id org.couchdb.user:ca_daemon --data '{"type": "user", "name": "ca_daemon", "roles": ["certs"], "password": "sshhhh"}' # # update a user: # couch-doc-update --db _users --id org.couchdb.user:ca_daemon --data '{"password":"sssshhh"}' # # To update the _users DB on bigcouch, you must connect to port 5986 instead of the default couchdb port 5984 # # delete a doc: # couch-doc-update --delete --db invite_codes --id dfaf0ee65670c16d5a9161dc86f3bff8 # begin; require 'rubygems'; rescue LoadError; end # optionally load rubygems require 'couchrest' def main db, id, data, delete = process_options result = if delete delete_document(db, id) else set_document(db, id, data) end exit 0 if result['ok'] raise StandardError.new(result.inspect) rescue StandardError => exc db_without_password = db.to_s.sub(/:[^\/]*@/, ':PASSWORD_HIDDEN@') indent = " " log "ERROR: " + exc.to_s log indent + $@[0..4].join("\n#{indent}") log indent + "Failed writing to #{db_without_password}/#{id}" exit 1 end def log(message) $stderr.puts message Syslog.open('couch-doc-update') do |logger| logger.log(Syslog::LOG_CRIT, message) end end def process_options # # parse options # host = nil db_name = nil doc_id = nil new_data = nil filename = nil netrc_file = nil delete = false loop do case ARGV[0] when '--host' then ARGV.shift; host = ARGV.shift when '--db' then ARGV.shift; db_name = ARGV.shift when '--id' then ARGV.shift; doc_id = ARGV.shift when '--data' then ARGV.shift; new_data = ARGV.shift when '--file' then ARGV.shift; filename = ARGV.shift when '--netrc-file' then ARGV.shift; netrc_file = ARGV.shift when '--delete' then ARGV.shift; delete = true when /^-/ then usage("Unknown option: #{ARGV[0].inspect}") else break end end usage("Missing required option") unless db_name && doc_id && (new_data || delete) unless delete new_data = MultiJson.load(new_data) new_data.merge!(read_file(filename)) if filename end db = CouchRest.database(connection_string(db_name, host, netrc_file)) return db, doc_id, new_data, delete end def read_file(filename) data = MultiJson.load( IO.read(filename) ) # strip off _id and _rev to avoid conflicts data.delete_if {|k,v| k.start_with? '_'} end # # update document # def set_document(db, id, data) attempt ||= 1 doc = get_document(db, id) if doc doc.id ||= id update_document(db, doc, data) else create_document(db, id, data) end rescue RestClient::Conflict # retry once, reraise if that does not work raise if attempt > 1 attempt += 1 retry end COUCH_RESPONSE_OK = { 'ok' => true } # Deletes document, if exists, with retry def delete_document(db, id) attempts ||= 1 doc = get_document(db, id) if doc db.delete_doc(doc) else COUCH_RESPONSE_OK end rescue RestClient::ExceptionWithResponse => e if attempts < 6 && !e.response.nil? && RETRY_CODES.include?(e.response.code) attempts += 1 sleep 10 retry else raise e end end def get_document(db, doc_id) begin db.get(doc_id) rescue RestClient::ResourceNotFound nil end end # if the response status code is one of these # then retry instead of failing. RETRY_CODES = [500, 422].freeze def update_document(db, doc, data) attempts ||= 1 doc.reject! {|k,v| !["_id", "_rev"].include? k} doc.merge! data db.save_doc(doc) rescue RestClient::ExceptionWithResponse => e if attempts < 6 && !e.response.nil? && RETRY_CODES.include?(e.response.code) attempts += 1 sleep 10 retry else raise e end end def create_document(db, doc_id, data) attempts ||= 1 data["_id"] = doc_id db.save_doc(data) rescue RestClient::ExceptionWithResponse => e if attempts < 6 && !e.response.nil? && RETRY_CODES.include?(e.response.code) attempts += 1 sleep 10 retry else raise e end end def connection_string(database, host, netrc_file = nil) protocol = "http" #hostname = "127.0.0.1" port = "5984" username = "admin" password = "" netrc = File.read(netrc_file || '/etc/couchdb/couchdb.netrc') netrc.scan(/\w+ [\w\.]+/).each do |key_value| key, value = key_value.split ' ' case key when "machine" then host ||= value + ':' + port when "login" then username = value when "password" then password = value end end host ||= '127.0.0.1:5984' "%s://%s:%s@%s/%s" % [protocol, username, password, host, database] end def usage(s) $stderr.puts(s) $stderr.puts("Usage: #{File.basename($0)} --host <host> --db <db> --id <doc_id> --data <json> [--file <file>] [--netrc-file <netrc-file>]") $stderr.puts(" #{File.basename($0)} --host <host> --db <db> --id <doc_id> --delete [--netrc-file <netrc-file>]") exit(2) end main()