├── .gitignore ├── scripts ├── postinstall.sh ├── uninstall-autocomplete.sh └── install-autocomplete.sh ├── src ├── config.js ├── complete.js ├── health.js ├── list.js ├── es_request.js ├── shard_allocation.js ├── indexes.js └── nodes.js ├── README.md ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | wip_data 4 | -------------------------------------------------------------------------------- /scripts/postinstall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | cat << EOF 6 | To enable autocompletion automatically, run 'install-escli-completion' 7 | 8 | If you want to install completion manually (requires root privileges): 9 | 10 | clever --bash-autocomplete-script "\$(which clever)" > /usr/share/bash-completion/completions/escli 11 | clever --zsh-autocomplete-script "\$(which clever)" > /usr/share/zsh/site-functions/_escli 12 | 13 | You can uninstall autocompletion at any moment by running 'uninstall-escli-completion' 14 | EOF 15 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | var env = require('common-env')(); 2 | var config = env.getOrElseAll({ 3 | user: { 4 | $default: null, 5 | $aliases: ['ES_CLI_USER'], 6 | $type: env.types.String 7 | }, 8 | password: { 9 | $default: null, 10 | $aliases: ['ES_CLI_PASSWORD'], 11 | $type: env.types.String 12 | }, 13 | host: { 14 | $default: 'localhost:9200', 15 | $aliases: ['ES_CLI_HOST'], 16 | $type: env.types.String 17 | }, 18 | https: { 19 | $default: false, 20 | $aliases: ['ES_CLI_SSL'], 21 | $type: env.types.Boolean 22 | } 23 | }); 24 | 25 | module.exports = config; 26 | -------------------------------------------------------------------------------- /scripts/uninstall-autocomplete.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | uninstall_zsh_root() { 6 | echo "Removing bash completion script" 7 | rm -f "/usr/share/zsh/site-functions/_escli" 8 | } 9 | 10 | uninstall_bash_root() { 11 | echo "Removing zsh completion script" 12 | rm -f "/usr/share/bash-completion/completions/escli" 13 | } 14 | 15 | uninstall() { 16 | uninstall_zsh_root 17 | uninstall_bash_root 18 | } 19 | 20 | main() { 21 | if [ "$(id -u)" -ne 0 ]; then 22 | echo "This program requires root privileges" >&2 23 | echo "Please run it as root or try either 'pkexec ${0}' or 'sudo ${0}'" >&2 24 | exit 1 25 | fi 26 | uninstall 27 | } 28 | 29 | main 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # escli 2 | escli si a CLI tool to manage elastic search cluster, rely on the awesome cliparse toolkit by @clementd 3 | 4 | It's an open WIP with current features as : 5 | * enable/;disable shard allocation 6 | * several simple list of ES objects 7 | * reallocate all shard from one node in the objective of kill it gracefully 8 | * open/close indexes 9 | 10 | Configuration is made using common-env by @FGRibreau My use case : autoenv + autojump 11 | 12 | Internal is made with promise 13 | 14 | install with 15 | `npm install escli --global 16 | ` 17 | 18 | Simple elastic search management command line. Configure with Env variable, using ES_CLI_HOST and optionaly ES_CLI_USER and ES_CLI_PASSWORD 19 | 20 | try 21 | `escli --help` 22 | -------------------------------------------------------------------------------- /src/complete.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var cliparse = require("cliparse"); 3 | 4 | 5 | module.exports = function(config, es_request) { 6 | 7 | return { 8 | nodes_complete: function() { 9 | return es_request.r({ 10 | dpath: '_nodes' 11 | }).then(function(nodes_res) { 12 | var data = JSON.parse(nodes_res); 13 | return cliparse.autocomplete.words(_.pluck(data.nodes, 'name')); 14 | }); 15 | }, 16 | indices_complete: function() { 17 | return es_request.r({ 18 | dpath: '_cat/indices?h=i' 19 | }).then(function(_res) { 20 | var iii = _res.split('\n'); 21 | iii.push('_all'); 22 | return cliparse.autocomplete.words(iii); 23 | }); 24 | } 25 | 26 | 27 | } 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | var cliparse = require("cliparse"); 4 | var config = require("./src/config.js"); 5 | var es_request = require("./src/es_request.js")(config); 6 | var complete = require("./src/complete.js")(config, es_request); 7 | 8 | var shard_allocation = require('./src/shard_allocation.js')(config, es_request, complete); 9 | var list = require('./src/list.js')(config, es_request, complete); 10 | var nodes = require('./src/nodes.js')(config, es_request, complete); 11 | var indexes = require('./src/indexes.js')(config, es_request, complete); 12 | var health = require('./src/health.js')(config, es_request, complete); 13 | 14 | 15 | 16 | var escli = cliparse.cli({ 17 | name: "escli", 18 | description: "Simple elastic search management command line. Configure with Env variable, using ES_CLI_HOST and optionaly ES_CLI_USER and ES_CLI_PASSWORD", 19 | commands: [ 20 | shard_allocation, 21 | list, 22 | nodes, 23 | indexes, 24 | health 25 | ] 26 | }); 27 | 28 | 29 | 30 | cliparse.parse(escli); 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "escli", 3 | "version": "0.1.20", 4 | "description": "Set of admin CLI tools for elastic search management", 5 | "main": "index.js", 6 | "scripts": { 7 | "postinstall": "./scripts/postinstall.sh" 8 | }, 9 | "bin": { 10 | "escli": "./index.js", 11 | "install-escli-completion": "./scripts/install-autocomplete.sh", 12 | "uninstall-escli-completion": "./scripts/uninstall-autocomplete.sh" 13 | }, 14 | "keywords": [ 15 | "cli", 16 | "elasticsearch", 17 | "admin" 18 | ], 19 | "author": "Quentin ADAM ", 20 | "license": "MIT", 21 | "dependencies": { 22 | "cliparse": "^0.2.5", 23 | "colors": "^1.1.2", 24 | "common-env": "^5.2.0", 25 | "lodash": "^3.10.1", 26 | "request": "^2.67.0", 27 | "request-promise": "^1.0.2" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git@github.com:CleverCloud/escli.git" 32 | }, 33 | "bugs": { 34 | "url": "https://github.com/CleverCloud/escli/issues" 35 | }, 36 | "homepage": "https://github.com/CleverCloud/escli" 37 | } 38 | -------------------------------------------------------------------------------- /src/health.js: -------------------------------------------------------------------------------- 1 | var cliparse = require("cliparse"); 2 | var colors = require("colors"); 3 | 4 | module.exports = function(config, es_request) { 5 | 6 | 7 | var color_parser = function(s, status) { 8 | switch (status) { 9 | case 'green': 10 | return s.green 11 | break; 12 | case 'yellow': 13 | return s.yellow 14 | break; 15 | case 'red': 16 | return s.red 17 | break; 18 | default: 19 | return s 20 | } 21 | } 22 | 23 | var cluster_health = function() { 24 | return es_request.r({ 25 | dpath: '_cluster/health' 26 | }).then(function(res) { 27 | var data = JSON.parse(res); 28 | console.log(color_parser(data.cluster_name.bold, data.status)); 29 | console.log(color_parser('cluster status is ' + data.status, data.status)); 30 | console.log(data.number_of_nodes + ' nodes with ' + data.number_of_data_nodes + ' data nodes'); 31 | }); 32 | }; 33 | 34 | 35 | 36 | 37 | return cliparse.command( 38 | "health", { 39 | description: "various metrics about cluster health" 40 | }, cluster_health); 41 | }; 42 | -------------------------------------------------------------------------------- /src/list.js: -------------------------------------------------------------------------------- 1 | var cliparse = require("cliparse"); 2 | 3 | module.exports = function(config, es_request) { 4 | 5 | var shard_list = function() { 6 | return es_request.r({ 7 | dpath: '_cat/shards?v' 8 | }).then(console.log); 9 | }; 10 | 11 | var index_list = function() { 12 | return es_request.r({ 13 | dpath: '_cat/indices?v' 14 | }).then(console.log); 15 | }; 16 | 17 | var nodes_list = function() { 18 | return es_request.r({ 19 | dpath: '_cat/nodes?v' 20 | }).then(console.log); 21 | }; 22 | 23 | 24 | 25 | return cliparse.command( 26 | "list", { 27 | description: "view list of various ES management data", 28 | commands: [ 29 | cliparse.command( 30 | "shards", { 31 | description: "view list of shards" 32 | }, shard_list), 33 | cliparse.command( 34 | "indexes", { 35 | description: "view list of indexes" 36 | }, index_list), 37 | cliparse.command( 38 | "nodes", { 39 | description: "view list of nodes" 40 | }, nodes_list) 41 | ] 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /src/es_request.js: -------------------------------------------------------------------------------- 1 | var rp = require('request-promise'); 2 | 3 | module.exports = function(config) { 4 | var es_request = {}; 5 | 6 | es_request.direct_request = function(options) { 7 | var o = options; 8 | if (config.password != null) { 9 | o.auth = { 10 | 'user': config.user, 11 | 'pass': config.password, 12 | 'sendImmediately': false 13 | }; 14 | } 15 | o.url = 'http' + (config.https ? 's' : '') + '://' + config.host + '/' + options.dpath; 16 | return o; 17 | }; 18 | 19 | es_request.r = function(options){ 20 | return rp(es_request.direct_request(options)); 21 | }; 22 | 23 | es_request.shard_allocation = function(activated) { 24 | var o = es_request.direct_request({ 25 | dpath: '_settings', 26 | method: 'PUT', 27 | json: { 28 | "index.routing.allocation.disable_allocation": activated 29 | } 30 | }) 31 | return rp(o) 32 | }; 33 | 34 | es_request.nodes = function(activated) { 35 | var o = es_request.direct_request({ 36 | dpath: '_nodes' 37 | }) 38 | return rp(o) 39 | }; 40 | 41 | return es_request; 42 | 43 | }; 44 | -------------------------------------------------------------------------------- /src/shard_allocation.js: -------------------------------------------------------------------------------- 1 | var cliparse = require("cliparse"); 2 | 3 | module.exports = function(config, es_request) { 4 | 5 | var shard_allocation_enable = function() { 6 | return es_request.r({ 7 | dpath: '_settings', 8 | method: 'PUT', 9 | json: { 10 | "index.routing.allocation.disable_allocation": false//, 11 | // 'cluster.routing.rebalance.enable': 'all' 12 | } 13 | }).then(console.log); 14 | }; 15 | 16 | var shard_allocation_disable = function() { 17 | return es_request.r({ 18 | dpath: '_settings', 19 | method: 'PUT', 20 | json: { 21 | "index.routing.allocation.disable_allocation": true //, 22 | // 'cluster.routing.rebalance.enable': 'none' 23 | } 24 | }).then(console.log); 25 | }; 26 | 27 | 28 | 29 | return cliparse.command( 30 | "shard_allocation", { 31 | description: "shards allocation management", 32 | commands: [ 33 | cliparse.command( 34 | "enable", { 35 | description: "enable auto shards allocation" 36 | }, shard_allocation_enable), 37 | cliparse.command( 38 | "disable", { 39 | description: "disable auto shards allocation" 40 | }, shard_allocation_disable) 41 | ] 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /src/indexes.js: -------------------------------------------------------------------------------- 1 | var cliparse = require("cliparse"); 2 | 3 | module.exports = function(config, es_request, complete) { 4 | 5 | var open_index = function(call_data) { 6 | if (call_data.args.length < 1) { 7 | return "args invalid, problem" 8 | } 9 | var iname = call_data.args[0]; 10 | 11 | return es_request.r({ 12 | dpath: iname + '/_open', 13 | method: 'POST' 14 | }).then(console.log); 15 | }; 16 | 17 | var closed_index = function(call_data) { 18 | if (call_data.args.length < 1) { 19 | return "args invalid, problem" 20 | } 21 | var iname = call_data.args[0]; 22 | 23 | return es_request.r({ 24 | dpath: iname + '/_closed', 25 | method: 'POST' 26 | }).then(console.log); 27 | }; 28 | 29 | var Index_name_Argument = cliparse.argument( 30 | "index-name", { 31 | defaultValue: '_all', 32 | description: "index name you want to act on, put _all if you want to act on all", 33 | complete: complete.indices_complete 34 | } 35 | ); 36 | 37 | 38 | 39 | 40 | return cliparse.command( 41 | "index", { 42 | description: "act on indexes of the cluster", 43 | commands: [ 44 | cliparse.command( 45 | "open", { 46 | description: "open or all if not define", 47 | args: [Index_name_Argument] 48 | }, open_index), 49 | cliparse.command( 50 | "close", { 51 | description: "close or all if not define", 52 | args: [Index_name_Argument] 53 | }, closed_index) 54 | ] 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /scripts/install-autocomplete.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | install_bash_root() { 6 | local wrapper="${1}" 7 | local us="${2}" 8 | local compdir="/usr/share/bash-completion/completions" 9 | 10 | echo "Installing bash completion script" 11 | mkdir -p "${compdir}" 12 | escli --bash-autocomplete-script "${us}" | "${wrapper}" tee "${compdir}"/escli >/dev/null 13 | } 14 | 15 | install_zsh_root() { 16 | local wrapper="${1}" 17 | local us="${2}" 18 | local compdir="/usr/share/zsh/site-functions" 19 | 20 | echo "Installing zsh completion script" 21 | mkdir -p "${compdir}" 22 | escli --zsh-autocomplete-script "${us}" | "${wrapper}" tee "${compdir}"/_escli >/dev/null 23 | } 24 | 25 | install() { 26 | local wrapper="${1}" 27 | local us 28 | 29 | us="$(which escli)" 30 | 31 | if which bash >/dev/null 2>&1; then 32 | install_bash_root "${wrapper}" "${us}" 33 | fi 34 | if which zsh >/dev/null 2>&1; then 35 | install_zsh_root "${wrapper}" "${us}" 36 | fi 37 | 38 | cat <<"EOF" 39 | You can uninstall completion scripts at any time by running uninstall-escli-completion 40 | EOF 41 | } 42 | 43 | main() { 44 | local wrapper 45 | local wrappers=( pkexec sudo ) 46 | 47 | for w in "${wrappers[@]}"; do 48 | wrapper="$(which "${w}" 2>/dev/null || true)" 49 | [ "x${wrapper}" != "x" ] && break 50 | done 51 | 52 | if [ "x${wrapper}" == "x" ]; then 53 | echo "This program requires root privileges and neither of \"${wrappers[@]}\" is installed" >&2 54 | echo "Please run it as root" >&2 55 | exit 1 56 | fi 57 | 58 | install "${wrapper}" 59 | } 60 | 61 | main 62 | -------------------------------------------------------------------------------- /src/nodes.js: -------------------------------------------------------------------------------- 1 | var cliparse = require("cliparse"); 2 | var _ = require("lodash"); 3 | 4 | module.exports = function(config, es_request, complete) { 5 | 6 | var nodes_remove_allocation = function(call_data) { 7 | if (call_data.args.length < 1) { 8 | return "You must define a node name"; 9 | } 10 | var node_name = call_data.args[0]; 11 | 12 | var number_of_shards = call_data.args[1]; 13 | 14 | return es_request.r({ 15 | dpath: '_cluster/state' 16 | }).then(function(iiii) { 17 | var data = JSON.parse(iiii); 18 | 19 | var node_key = _.result(_.find(_.map(data.nodes, function(v, k) { 20 | v.id = k; 21 | return v; 22 | }), function(v, k) { 23 | return v.name == node_name; 24 | }), 'id'); 25 | if (node_key == undefined) { 26 | console.log("unvalid node name") 27 | return "node name is not valid" 28 | } else { 29 | console.log("the node id for " + node_name + " is " + node_key); 30 | } 31 | 32 | var onodes = _.without(_.keys(data.nodes), node_key); 33 | 34 | console.log('shards to move') 35 | var t = _.filter(data.routing_nodes.nodes[node_key], function(i) { 36 | return i.state != 'RELOCATING'; 37 | }); 38 | if (number_of_shards > t.length) { 39 | number_of_shards = t.length; 40 | } 41 | var move_orders = _.map(t.slice(0, number_of_shards), function(i) { 42 | return { 43 | 'move': { 44 | 'index': i.index, 45 | 'shard': i.shard, 46 | 'from_node': node_key, 47 | 'to_node': _.sample(onodes) 48 | } 49 | } 50 | }) 51 | 52 | console.log(move_orders) 53 | 54 | console.log('launch relocation command') 55 | 56 | var ff = es_request.r({ 57 | dpath: '_cluster/reroute?explain', 58 | method: 'POST', 59 | json: { 60 | commands: move_orders 61 | 62 | } 63 | }); 64 | 65 | return ff; 66 | }).then(function(d) { 67 | return JSON.stringify(d, null, 2); 68 | }).then(console.log); 69 | }; 70 | 71 | 72 | var nodes_unasigned_allocation = function(call_data) { 73 | if (call_data.args.length < 1) { 74 | return "You must define a node name"; 75 | } 76 | var node_name = call_data.args[0]; 77 | var number_of_shards = call_data.args[1]; 78 | 79 | return es_request.r({ 80 | dpath: '_cluster/state' 81 | }).then(function(iiii) { 82 | var data = JSON.parse(iiii); 83 | 84 | var node_key = _.result(_.find(_.map(data.nodes, function(v, k) { 85 | v.id = k; 86 | return v; 87 | }), function(v, k) { 88 | return v.name == node_name; 89 | }), 'id'); 90 | if (node_key == undefined) { 91 | console.log("unvalid node name") 92 | return "node name is not valid" 93 | } else { 94 | console.log("the node id for " + node_name + " is " + node_key); 95 | } 96 | 97 | console.log('shards to allocate') 98 | var move_orders = _.map(_.sample(data.routing_nodes.unassigned, number_of_shards), function(i) { 99 | return { 100 | 'allocate': { 101 | 'index': i['index'], 102 | 'shard': i.shard, 103 | 'node': node_key 104 | } 105 | } 106 | }) 107 | 108 | console.log(move_orders) 109 | 110 | console.log('launch allocation command') 111 | 112 | var ff = es_request.r({ 113 | dpath: '_cluster/reroute?explain', 114 | method: 'POST', 115 | json: { 116 | commands: move_orders 117 | 118 | } 119 | }); 120 | 121 | return ff; 122 | }).then(function(d) { 123 | return JSON.stringify(d, null, 2); 124 | }).then(console.log); 125 | }; 126 | 127 | 128 | var from_node_to_node = function(call_data) { 129 | if (call_data.args.length < 3) { 130 | return "You must define a node1 to node2 arguments"; 131 | } 132 | 133 | var node_from = call_data.args[0]; 134 | var node_to = call_data.args[2]; 135 | var number_of_shards = call_data.args[3]; 136 | 137 | return es_request.r({ 138 | dpath: '_cluster/state' 139 | }).then(function(iiii) { 140 | var data = JSON.parse(iiii); 141 | 142 | var node_key_from = _.result(_.find(_.map(data.nodes, function(v, k) { 143 | v.id = k; 144 | return v; 145 | }), function(v, k) { 146 | return v.name == node_from; 147 | }), 'id'); 148 | 149 | var node_key_to = _.result(_.find(_.map(data.nodes, function(v, k) { 150 | v.id = k; 151 | return v; 152 | }), function(v, k) { 153 | return v.name == node_to; 154 | }), 'id'); 155 | 156 | 157 | if (node_to == undefined || node_from == undefined) { 158 | console.log("unvalid node name") 159 | return "node name is not valid" 160 | } else { 161 | console.log("move 10 shards from " + node_from + " to " + node_to) 162 | console.log("the node id for " + node_from + " is " + node_key_from); 163 | console.log("the node id for " + node_to + " is " + node_key_to); 164 | } 165 | 166 | console.log('shards to move') 167 | var move_orders = _.map(_.sample(_.filter(data.routing_nodes.nodes[node_key_from], function(i) { 168 | return i.state != 'RELOCATING'; 169 | }), number_of_shards), function(i) { 170 | return { 171 | 'move': { 172 | 'index': i.index, 173 | 'shard': i.shard, 174 | 'from_node': node_key_from, 175 | 'to_node': node_key_to 176 | } 177 | } 178 | }) 179 | 180 | console.log(move_orders) 181 | 182 | console.log('launch relocation command') 183 | 184 | var ff = es_request.r({ 185 | dpath: '_cluster/reroute?explain', 186 | method: 'POST', 187 | json: { 188 | commands: move_orders 189 | 190 | } 191 | }); 192 | 193 | return ff; 194 | }).then(function(d) { 195 | return JSON.stringify(d, null, 2); 196 | }).then(console.log); 197 | }; 198 | 199 | 200 | var node_Argument = cliparse.argument( 201 | "node-name", { 202 | defaultValue: '', 203 | description: "node name you want to decommision", 204 | complete: complete.nodes_complete 205 | } 206 | ); 207 | 208 | var to_Argument = cliparse.argument( 209 | "to", { 210 | defaultValue: 'to', 211 | description: "just write \"to\"" 212 | } 213 | ); 214 | 215 | var number_Argument = cliparse.argument("int", { 216 | default: 10, 217 | parser: cliparse.parsers.intParser, 218 | description: "number of shards to move" 219 | }); 220 | 221 | 222 | 223 | 224 | return cliparse.command( 225 | "nodes", { 226 | description: "nodes operations", 227 | commands: [ 228 | cliparse.command( 229 | "empty_node", { 230 | args: [node_Argument, number_Argument], 231 | description: "reasign shards out of this node" 232 | }, nodes_remove_allocation), 233 | cliparse.command( 234 | "get_unasigned", { 235 | args: [node_Argument, number_Argument], 236 | description: "get N unasigned shards on this node" 237 | }, nodes_unasigned_allocation), 238 | cliparse.command( 239 | "move_shards", { 240 | args: [node_Argument, to_Argument, node_Argument, number_Argument], 241 | description: "move [x] shards from node1 to node2" 242 | }, from_node_to_node) 243 | ] 244 | }); 245 | }; 246 | --------------------------------------------------------------------------------