{"id":13624956,"url":"https://github.com/tsmith/node-control","last_synced_at":"2025-04-16T01:33:07.583Z","repository":{"id":57206621,"uuid":"787801","full_name":"tsmith/node-control","owner":"tsmith","description":"Scripted system admin and deployment for many remote machines in parallel via ssh with Node","archived":false,"fork":false,"pushed_at":"2012-01-27T23:35:27.000Z","size":149,"stargazers_count":389,"open_issues_count":9,"forks_count":33,"subscribers_count":13,"default_branch":"master","last_synced_at":"2025-03-27T21:31:59.419Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tsmith.png","metadata":{"files":{"readme":"README","changelog":"CHANGELOG","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2010-07-21T00:59:19.000Z","updated_at":"2024-06-07T22:04:56.000Z","dependencies_parsed_at":"2022-09-08T14:22:10.222Z","dependency_job_id":null,"html_url":"https://github.com/tsmith/node-control","commit_stats":null,"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tsmith%2Fnode-control","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tsmith%2Fnode-control/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tsmith%2Fnode-control/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tsmith%2Fnode-control/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tsmith","download_url":"https://codeload.github.com/tsmith/node-control/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249182488,"owners_count":21226074,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T21:01:48.688Z","updated_at":"2025-04-16T01:33:07.574Z","avatar_url":"https://github.com/tsmith.png","language":"JavaScript","funding_links":[],"categories":["Others"],"sub_categories":[],"readme":"DESCRIPTION\n\nDefine tasks for system administration or code deployment, then execute them on\none or many remote machines simultaneously. Strong logging creates a complete\naudit trail of commands executed on remote machines in logs easily analyzed by\nstandard text manipulation tools. \n\nnode-control depends only on OpenSSH and Node on the local control machine.\nRemote machines simply need a standard sshd daemon.\n\n\n\nQUICK EXAMPLE\n\nIf you want to control remote machines from individual scripts without the\ntasks system, see QUICK EXAMPLE WITHOUT TASKS. Otherwise, to get the current\ndate from the two machines listed in the 'mycluster' config with a single\ncommand:\n\nvar control = require('control'),\n    task = control.task;\n\ntask('mycluster', 'Config for my cluster', function () {\n    var config = {\n        'a.domain.com': {\n            user: 'alogin'\n        },\n        'b.domain.com': {\n            user: 'blogin',\n            sshOptions: ['-p 44'] // sshd daemon on non-standard port\n        }\n    };\n\n    return control.controllers(config);\n});\n\n\ntask('date', 'Get date', function (controller) {\n    controller.ssh('date');\n});\n\ncontrol.begin();\n\n\nIf saved in a file named 'controls.js', run with:\n\nnode controls.js mycluster date\n\n\nEach machine is contacted in parallel, date is executed, and the output from\nthe remote machine is printed to the console. Example console output:\n\n Performing mycluster\n Performing date for a.domain.com\na.domain.com:alogin:ssh: date\n Performing date for b.domain.com\nb.domain.com:blogin:ssh: date\na.domain.com:stdout: Sun Jul 18 13:30:50 UTC 2010\nb.domain.com:stdout: Sun Jul 18 13:30:51 UTC 2010\na.domain.com:exit: 0\nb.domain.com:exit: 0\n           \n\nEach line of output is labeled with the address of the machine the command was\nexecuted on. The actual command sent and the user used to send it is\ndisplayed. stdout and stderr output of the remote process is identified\nas well as the final exit code of the local ssh command. Each command, stdout,\nstderr, and exit line also appears timestamped in a control.log file in the\ncurrent working directory.\n\nSee CODE DEPLOYMENT EXAMPLE for an example of deploying an application to\nremote servers.\n\n\n\nINSTALLATION\n\nIf you use npm:\n\nnpm install control\n\nIf you do not use npm, clone this repository with git or download the latest\nversion using the GitHub repository Downloads link. Then use as a standard Node\nmodule by requiring the node-control directory.\n\n\n\nEXAMPLE CONTROLS\n\nAs you read this documentation, you may find it useful to refer to the\nexample/controls.js file. Its work tasks cover a variety of advanced usage. The\nconfig tasks use your local machine as a mock remote machine or cluster, so if\nyou run an sshd daemon locally, you can run the controls against your own\nmachine to experiment. \n\n\n\nCONFIG TASKS\n\nWhen using tasks, you always identify two tasks on the command line for remote\noperations. The first task is the config task and the second task is the work\ntask. Config tasks have a name, description, and function that will be called\nonce:\n\ntask('mycluster', 'Config for my cluster', function () {\n\n\nThe config task function must return an array of controllers (objects that\nextend the control.controller prototype, described further in CONTROLLERS).\nEach controller in the array controls a single machine and optionally has its\nown properties. \n\nConfig tasks enable definition of reusable work tasks independent of the\nmachines they will control. For example, if you have a staging environment with\ndifferent machines than your production environment, you can create two\ndifferent config tasks, each returning controllers for machines in the\nrespective environment, yet use the same deploy work task:\n\nnode controls.js stage deploy ~/myapp/releases/myapp-1.0.tgz\n\nnode controls.js production deploy ~/myapp/releases/myapp-1.0.tgz\n\n\nIf all the machines in a cluster share common properties, you can extend the\ncontrol.controller prototype and pass the new prototype into controllers() as\nthe second argument. For example, if all the machines in your cluster run sshd\non a non-standard port instead of just one as in QUICK EXAMPLE:\n\ntask('mycluster', 'Config for my cluster', function () {\n    var shared = Object.create(control.controller),\n        config = {\n            'a.domain.com': {\n                user: 'alogin'\n            },\n            'b.domain.com': {\n                user: 'blogin'\n            }\n        };\n\n    shared.sshOptions = ['-p 44'];\n\n    return control.controllers(config, shared);\n});\n\n\ncontrollers() will return an array of controllers that prototypically inherit\nfrom the shared prototype instead of the base prototype, each having\ncontroller-specific properties as defined in the JSON notation. In this case,\nboth controllers will effectively have sshOptions = ['-p 44'], but different\nuser names.\n\nIf all machines in your cluster have the same properties, can you pass an array\nof addresses as the first argument to controllers(). For example, if all the\nmachines your cluster run sshd on a non-standard port and you use the same\nlogin on each:\n\ntask('mycluster', 'Config for my cluster', function () {\n    var shared = Object.create(control.controller), \n        addresses = [ 'a.domain.com',\n                      'b.domain.com',\n                      'c.domain.com' ];\n    shared.user = 'mylogin'; \n    shared.sshOptions = ['-p 44'];\n    return control.controllers(addresses, shared);\n});\n\n\nAlternatively, you can build up your list of controllers without the use of\ncontrollers():\n\ntask('mycluster', 'Config for my cluster', function () {\n    var controllers = [],\n        shared = Object.create(control.controller), // Extend prototype \n        a, b;\n\n    shared.sshOptions = ['p 44'];\n\n    a = Object.create(shared); // Extend shared prototype\n    a.address = 'a.domain.com';\n    a.user = 'alogin'; \n    controllers.push(a);\n\n    b = Object.create(shared);\n    b.address = 'b.domain.com';\n    b.user = 'blogin'; \n    controllers.push(b);\n\n    return controllers;\n});\n\n\n\nWORK TASKS\n\nWork tasks define logic to drive each controller returned by the config task.\nThey have a name, description, and a callback that will execute independently\nand simultaneously for each controller:\n\ntask('date', 'Get date', function (controller) {\n\n\nArguments on the command line after the name of the work task become arguments\nto the work task's function. With this task:\n\ntask('deploy', 'Deploy my app', function (controller, release) {\n\n\nThis command:\n\nnode controls.js stage deploy ~/myapp/releases/myapp-1.0.tgz\n\n\nResults in: \n\nrelease = '~/myapp/releases/myapp-1.0.tgz'\n\n\nMore than one argument is possible:\n\ntask('deploy', 'Deploy my app', function (controller, release, tag) {\n\n\n\nTASK EXECUTION\n\nTo execute the tasks identified on the command line, use the begin() method\nafter you have defined all your config and work tasks:\n\nvar control = require('control');\n... // Define tasks\ncontrol.begin();\n\n\nbegin() calls the first (config) task identified on the command line to get the\narray of controllers, then calls the second (work) task with each of the\ncontrollers. If you run a control script and nothing happens, check if the\nscript calls begin().\n\n\n\nCONTROLLERS\n\nnode-control provides a base controller prototype as control.controller, which\nall controllers must extend. To create controllers, use the controllers()\nmethod described in CONFIG TASKS or extend the base controller prototype and\nassign the controller a DNS or IP address, user if not the same as the local\nuser, and any other properties required by work tasks or further logic:\n\nvar controller = Object.create(control.controller);\ncontroller.address = 'a.domain.com'; // Machine to control\ncontroller.user = 'mylogin'; // Username on remote machine if not same as local\ncontroller.ips = [ // Example of property used by work task or further logic \n        '10.2.136.23',\n        '10.2.136.24',\n        '10.2.136.25',\n        '10.2.136.26',\n        '10.2.136.27' \n    ];\n\n\nThe base controller prototype provides ssh() and scp() methods for\ncommunicating with a controller's assigned remote machine.\n\nThe ssh() method takes one argument - the command to be executed on the\nremote machine. The scp method takes two arguments - the local file path and the\nremote file path. \n\nBoth ssh() and scp() methods are asynchronous and can additionally take a\ncallback function that is executed once the ssh or scp operation is complete.\nThis guarantees that the first operation completes before the next one begins\non that machine:\n\n    controller.scp(release, remoteDir, function () {\n        controller.ssh('tar xzvf ' + remotePath + ' -C ' + remoteDir, \n                function () {\n\n\nYou can chain callbacks as far as necessary.\n\nIf a command returns a non-zero exit code, the scp() and ssh() methods will log\nthe exit and exit code, but will not call the callback, ending any further\noperations on that machine. This avoids doing further harm where a callback may\nassume a successful execution of a previous command. However, you can specify\nan exit callback that will be called and receive the exit code if a non-zero\nexit occurs:\n\nfunction callback() { ... }\nfunction exitCallback(code) { ... }\n\ncontroller.ssh('date', callback, exitCallback);\n\n\nYou can make both callbacks the same callback function if you want to check the\nexit code and handle both zero and non-zero exits within a single callback.\n\n\n\nCUSTOM STDOUT \u0026 STDERR LISTENERS\n\nWhen running a command with ssh() on a remote device, controller objects listen\nto the stdout and stderr of the process running on the remote device through\nthe local ssh process, printing what is heard to console and log. You can\nattach your own listeners to these stdout and stderr streams to gather data to\nuse in your callback function:  \n\ntask('ondate', 'Different logic paths based on date', function (controller) {\n    var datestring = '';\n\n    controller.stdout.on('data', function (chunk) {\n        datestring += chunk.toString();\n    });\n\n    controller.ssh('date', function () {\n        console.log('  Date string is ' + datestring);\n        // Further logic dependent on value of datestring\n    });\n});\n\n\nRefer to Node's ReadableStream and EventEmitter documentation if the\nstdout.on() pattern looks unfamiliar. Controllers also provide a stderr.on()\nfor attaching custom listeners to the stderr stream.\n\nYou can respond to prompts and errors as they happen in the remote process\nthrough the remote process stdin stream, similar to expect. An example of\nresponding to a prompt through the stdin of the remote process:\n\ntask('stdin', 'Test controller stdin usage', function (controller) {\n    var stdout;\n\n    controller.stdout.on('data', function (chunk) {\n        chunk = chunk.toString(); // Assumes chunks come in full lines\n        if (chunk.match('^Enter data')) { // Assumes command uses this prompt\n            controller.stdin.write('hello\\n');\n        }\n    });\n\n    controller.ssh('acommand');\n});\n\n\nThe controller only uses custom listeners for the next ssh() or scp() call.\nFurther ssh() or scp() calls will not attach the custom listener unless it is\nreattached via controller.stdout.on() or controller.stderr.on() before the next\ncall. This avoids unanticipated usage of one-off listeners, such as filling the\ndatestring variable in the first example with the output of every subsequent\nssh() command executed by the controller.\n\n\n\nPERFORMING MULTIPLE TASKS\n\nA task can call other tasks using perform() and optionally pass arguments to\nthem:\n\nvar perform = require('control').perform;\n\ntask('mytask', 'My task description', function (controller, argument) {\n    perform('anothertask', controller, argument);\n\n\nperform() requires only the task name and the controller. Arguments are\noptional. If the other task supports it, optionally pass a callback function as\none of the arguments:\n\n    perform('anothertask', controller, function () {\n\n\nTasks that support asynchronous performance should call the callback function\nwhen done doing their own work. For example:\n\ntask('anothertask', 'My other task', function (controller, callback) {\n    controller.ssh('date', function () {\n        if (callback) {\n            callback();\n        }\n    });\n});\n\n\nThe peform() call can occur anywhere in a task, not just at the beginning.\n\n\n\nLISTING TASKS\n\nTo list all defined tasks with descriptions:\n\nnode controls.js mycluster list\n\n\n\nNAMESPACES\n\nUse a colon, dash, or similar convention when naming if you want to group tasks\nby name. For example:\n\ntask('bootstrap:tools', 'Bootstrap tools', function (controller) { \n...\ntask('bootstrap:compilers', 'Bootstrap compilers', function (controller) { \n\n\n\nSUDO\n\nTo use sudo, include sudo as part of your command:\n\ncontroller.ssh('sudo date');\n\n\nThis requires that sudo be installed on the remote machine and have requisite\npermissions setup.\n\n\n\nROLES\n\nSome other frameworks like Capistrano provide the notion of roles for different\nmachines. node-control does not employ a separate roles construct. Since\ncontrollers can have any properties defined on them in a config task, a\npossible pattern for roles if needed:\n\ntask('mycluster', 'Config for my cluster', function () {\n    var dbs = Object.create(control.controller),\n        apps = Object.create(control.controller);\n\n    dbs = {\n        user: 'dbuser',\n        role: 'db'\n    };\n\n    apps = {\n        user: 'appuser',\n        role: 'app'\n    };\n\n    dbs = control.controllers(['db1.domain.com', 'db2.domain.com'], dbs);\n    apps = control.controllers(['app1.domain.com', 'app2.domain.com'], apps);\n\n    return dbs.concat(apps); \n});\n\ntask('deploy', 'Deploy my system', function (controller, release) {\n    if (controller.role === 'db') {\n        // Do db deploy work\n    }\n\n    if (controller.role === 'app') {\n        // Do app deploy work\n    }\n});\n\n\n\nLOGS\n\nAll commands sent and responses received are logged with timestamps (from the\ncontrol machine's clock). By default, logging goes to a control.log file in the\nworking directory of the node process. However, you can override this in your\ncontrol script:\n\ntask('mycluster', 'Config for my cluster', function () {\n    var shared, addresses;\n    shared = {\n        user: 'mylogin',\n        logPath: '~/mycluster-control.log'\n    };\n    addresses = [ 'a.domain.com',\n                  'b.domain.com',\n                  'c.domain.com' ];\n    return control.controllers(addresses, shared); \n});\n\n\nSince each controller gets its own log property, every controller could\nconceivably have its own log fie. However, every line in the log file has a\nprefix that includes the controller's address so, for example: \n\ngrep a.domain.com control.log | less\n\n\nWould allow paging the log and seeing only lines pertaining to\na.domain.com.\n\n\nIf you send something you do not want to get logged (like a password) in a\ncommand, use the log mask:\n\ncontroller.logMask = secret;\ncontroller.ssh('echo ' + secret + ' \u003e file.txt');\n\n\nThe console and command log file will show the masked text as asterisks instead\nof the actual text.\n\n\n\nSSH\n\nTo avoid repeatedly entering passwords across possibly many machines, use\nstandard ssh keypair authentication. \n\nEach controller.ssh() call requires a new connection to the remote machine. To\nconfigure ssh to reuse a single connection, place this:\n\nHost *\nControlMaster auto \nControlPath ~/.ssh/master-%r@%h:%p\n\n\nIn your ssh config file (create if it does not exist):\n\n~/.ssh/config\n\n\nTo pass options to the ssh command when using ssh(), add the option or options\nas an array to the sshOptions property of the controller or controllers'\nprototype:\n\ncontroller.sshOptions = [ '-2', '-p 44' ];\n\n\nUse scpOptions in the same manner for scp().\n\n\n\nCONFIG TASK COMMAND LINE ARGUMENTS REWRITING\n\nConfig tasks receive a reference to the array of remaining arguments on the\ncommand line after the config task name is removed. Therefore, config tasks\ncan rewrite the command line arguments other than the config task name. Example:\n\nfunction configure(addresses) {\n    var shared;\n    shared = {\n        user: 'mylogin'\n    };\n    return control.controllers(addresses, shared); \n}\n\ntask('mycluster', 'Config for my cluster', function () {\n    var addresses = [ 'a.domain.com',\n                      'b.domain.com',\n                      'c.domain.com' ];\n    return configure(addresses); \n});\n\ntask('mymachine', 'Config for one machine from command line', function (args) {\n    return configure([args.shift()]); // From command line arguments rewriting\n});\n\n\nWith this set of config tasks, if there is an ad hoc need to run certain tasks\nagainst a single machine in the cluster, but otherwise have identical\nconfiguration as when run as part of the cluster, the machine address can be\nspecified on the command line:\n\nnode controls.js mymachine b.domain.com mytask x\n\n\nIn that case, the mymachine config task receives as args:\n\n['b.domain.com', 'mytask', 'x']\n\n\nThis is generally not necessary since you can edit the config task in the\ncontrol file at any time, but is available if config tasks need to have command\nline arguments or rewrite the work task name and its arguments on the fly.\n\n\n\nCODE DEPLOYMENT EXAMPLE\n\nA task that will upload a local compressed tar file containing a release of a\nnode application to a remote machine, untar it, and start the node application.\n\nvar path = require('path');\n\ntask('deploy', 'Deploy my app', function (controller, release) {\n    var basename = path.basename(release),\n        remoteDir = '/apps/',\n        remotePath = path.join(remoteDir, basename),\n        remoteAppDir = path.join(remoteDir, 'myapp');\n    controller.scp(release, remoteDir, function () {\n        controller.ssh('tar xzvf ' + remotePath + ' -C ' + remoteDir, \n                function () {\n            controller.ssh(\"sh -c 'cd \" + remoteAppDir + \" \u0026\u0026 node myapp.js'\"); \n        });\n    });\n});\n\nExecute as follows, for example:\n\nnode controls.js mycluster deploy ~/myapp/releases/myapp-1.0.tgz\n\n\nA full deployment solution would shut down the existing application and have\ndifferent directory conventions. node-control does not assume a particular\nstyle or framework. It provides tools to build a custom deployment strategy for\nyour application, system, or framework.\n\n\n\nQUICK EXAMPLE WITHOUT TASKS\n\nYou can create scripts to run individually instead of through the tasks system\nby using controllers() to create an array of controllers and then using\nthe controllers directly:\n\nvar control = require('../'),\n    shared = Object.create(control.controller),\n    i, l, controller, controllers;\n\nshared.user = 'mylogin';\ncontrollers = control.controllers(['a.domain.com', 'b.domain.com'], shared);\n\nfor (i = 0, l = controllers.length; i \u003c l; i += 1) {\n    controller = controllers[i];\n    controller.ssh('date');\n}\n\n\nIf saved in a file named 'controls.js', run with:\n\nnode controls.js\n\n\nSee example/taskless.js for a working example you can run against your local\nmachine if running a local sshd.\n\n\n\nCONTRIBUTORS\n\n* David Pratt (https://github.com/fairwinds)\n* Peter Lyons (https://github.com/focusaurus)\n\n\n\nFEEDBACK\n\nWelcome at node@thomassmith.com or the Node mailing list.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftsmith%2Fnode-control","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftsmith%2Fnode-control","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftsmith%2Fnode-control/lists"}