initial commit - opensourcing project
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.DS_Store
|
162
README.md
|
@ -1,2 +1,160 @@
|
|||
# alnoda-workspaces
|
||||
Work directly inside docker conntainers
|
||||
# Workspaces in docker
|
||||
|
||||
### Contents
|
||||
|
||||
* [About](#about)
|
||||
* [Quickstart](#quickstart)
|
||||
* [Available workspaces](#available-workspaces)
|
||||
* [Why dev environmet in docker](#why-dev-environmet-in-docker)
|
||||
* [Workspaces-in-docker vs. cloud IDE](#workspaces-in-docker-vs.-cloud-ide)
|
||||
* [Workspaces-in-docker vs. other docker workspaces](#workspaces-in-docker-vs.-other-docker-workspaces)
|
||||
* [Workspaces-in-docker principles](#workspaces-in-docker-principles)
|
||||
* [Contribution](#contribution)
|
||||
|
||||
|
||||
## About
|
||||
|
||||
Workspaces make development, experiments and workloads isolated in their own dockerized environments,
|
||||
enabling working directly inside the running docker containers, and managing numerous projects easily.
|
||||
|
||||
Workspaces include tools and software that make working inside docker nearly as convenient as working
|
||||
directly with local environment.
|
||||
|
||||
![Workspaces are amazing!](./workspaces/workspace-in-docker/img/workspace-demo.gif)
|
||||
|
||||
Workspace is not only a development environment. Some workspaces include lots of tools and packages, that otherwise would
|
||||
take lots of time to set up.
|
||||
|
||||
Workspaces are familar docker images and containers. Working with workspaces will not require learning any new tools,
|
||||
only will help to improve docker skills.
|
||||
|
||||
Workspaces allow moving complete environments between machinnes and cloud servers as easy as 2 clicks. With workspaces you can
|
||||
actually have 2 develpment machines, and sync them very easily. You can even work in the vary same workspace on your Mac laptop
|
||||
and Windows PC. And then you move the complete development or runtime environment to a cloud Linux server in under 1 minute.
|
||||
|
||||
Workspaces are great for collaboration. Imagine being able to share with your peer not only the codebase, but also to the whole environment
|
||||
with all the dependencies and config files.
|
||||
|
||||
Workspaces make containerization and deployment as easy as possible, without the need of building docker
|
||||
images again. We love containers for their ability to run on production environment exactly the same way
|
||||
they are running on "your laptop". We already run a lot in containers. Why not build our apps in containers as well?
|
||||
Then we can simply skip the extra step of building images again and again.
|
||||
|
||||
Workspaces can be used as true Linux terminals. Anywhere. On any system. In fact, workspaces include lots of tools that
|
||||
make woking in terminal a pleasure.
|
||||
|
||||
<p align="center">
|
||||
<img src="./workspaces/ubuntu-workspace/img/ubuntu-workspace.gif" alt="Ubuntu workspace" width="750">
|
||||
</p>
|
||||
|
||||
Workspaces are great to preserve sometnig that works. Imagin you have made an MVP. Something actually working. It is not enough just to
|
||||
commit the code. Save the complete workspace! With all environments, dependencies and configurations. Later it might save you a lot of time.
|
||||
|
||||
Workspaces are completely open-source and include only popular wide-spread open-source tools. No proprietary software.
|
||||
|
||||
## Quickstart
|
||||
|
||||
Launching new workspace is as easy as
|
||||
|
||||
```sh
|
||||
docker run --name space-1 -d -p 8020-8030:8020-8030 alnoda/workspace-in-docker
|
||||
```
|
||||
|
||||
that is it! Open workspace UI ***[http://localhost:8020](http://localhost:8020)***
|
||||
|
||||
or enter workspace terminal
|
||||
```sh
|
||||
docker exec -it space-1 /bin/zsh
|
||||
```
|
||||
|
||||
## Available workspaces
|
||||
|
||||
1. [`ubuntu-workspace`](./workspaces/ubuntu-workspace/README.md). Primarily intended as an advanced Ubuntu terminal that runs anywhere, this workspace works best
|
||||
when you need interactive linux, python or node shell for ad-hock tasks.
|
||||
|
||||
2. [`base-workspace`](./workspaces/base-workspace/README.md). Does not include IDE, and serves as a building base for other workspaces with different IDEs.
|
||||
|
||||
3. [`workspace-in-docker`](./workspaces/workspace-in-docker/README.md). Workspace-in-docker is a good choice if you want control and versatility. It is lightweight,
|
||||
includes open-source web-based version of Visual Studio Code, and a reasonable collection of tools that
|
||||
make working inside docker container nearly as convenient as working on local environment.
|
||||
Very customizable.
|
||||
|
||||
|
||||
## Why dev environmet in docker
|
||||
|
||||
Have your ever participated in several software (sysops, analytics, BI, data science, ML) projects at the same time?
|
||||
And each of them having different environments (dev, stage, prod), cloud config file, k8s cluster, ansible secret,
|
||||
terraform workspace... Managing such a collection of environments is so much easier
|
||||
if everything is packaged inside a docker container, that you can start and stop by one single command.
|
||||
|
||||
There are lots of limitations compared to local environment. For example, you will have to work only with WEB-based tools - this limits your choices
|
||||
and might not include your favourite shiny IDE, on which you spent so many months to configure and learn. Also a web-based IDE is always slower
|
||||
than its native alternative, and probably not as feature-rich.
|
||||
|
||||
But the benefits of isolating everything related to the specific project in one docker image that can be saved, versioned, moved to another machine,
|
||||
deployed on cloud server or shared with anyone, by far outweighs those limitations.
|
||||
|
||||
*Workspaces promote non-canonical way of using docker, taking advantage of features, we don't often use.*
|
||||
|
||||
We already run a lot in containers. Why not build our apps in containers as well?
|
||||
Workspace encapsulates nicely project or experiment with all the dependencies, helping to avoid situation when one environment can affect another.
|
||||
|
||||
> Docker is a great tool to bundle together all the things that are related to the same project. It is light-weight and you can start multiple workspaces.
|
||||
|
||||
We often over-engineer and waste time on something that we could do better. If we do something on our local machines, it becomes isolated, non-transferrable
|
||||
and not shareable. If we do the same work inside a running docker container, we can commit it to the new image and share with peers or run
|
||||
on cloud servers, and this is all - just 3 commands in terminal.
|
||||
|
||||
## Workspaces-in-docker vs. cloud IDE
|
||||
|
||||
There are other options that provide isolated environments and let work in cloud directly.
|
||||
Cloud9, CodeAnywhere, GitPod, GroomIDE you name it - are awesome and affordable services, that are even easier to use than workspaces-in-docker.
|
||||
But workspaces-in-docker have some serious advantages:
|
||||
|
||||
- workspaces-in-docker are transferrable, they can be used on both local machine as well as in cloud,
|
||||
being easily moved back and forth. You might not always have perfect internet, and running workspace on the local machine can be more convenient.
|
||||
And you can always move workspace to a powerfule server in cloud within couple minutes.
|
||||
|
||||
- workspaces-in-docker are more configurable. Tey are open-source docker images after all. Create new image from the workspace image,
|
||||
and you will have highly customized personal workspace.
|
||||
|
||||
Workspaces-in-docker is a middle ground between two extremes: the unwieldy static local environment - unseparable from your laptop,
|
||||
and euphemeral cloud environments that are much less versatile.
|
||||
|
||||
## Workspaces-in-docker vs. other docker workspaces
|
||||
|
||||
The idea of development in docker is not new at all. In fact there are other projects in GitHub
|
||||
that encourage users to work directly in the docker containers.
|
||||
|
||||
On the left side of the spectrum, there are WEB-based IDE, each of them having their own docker images.
|
||||
|
||||
On the right side there exist over-packed docker images for particular use-cases, that include way too many tools than probably needed.
|
||||
|
||||
Workspaces-in-docker take the niche in the middle. Instead of building one heavy unwieldy image that focuses on one specific use-case,
|
||||
we create a "constructor" stack of images build on top of each other for better control and ease of customization.
|
||||
|
||||
And we take security seriously. Workspaces-in-docker by default run under non-root user. Of course you can run or enter workspace as root too.
|
||||
This makes it secure to share with peers, or deploy production ready applications directly as workspace container.
|
||||
|
||||
## Workspaces-in-docker principles
|
||||
|
||||
When developing our dockerized workspaces, we try to set and follow some rules:
|
||||
|
||||
- worksapces should be easy to use and solve real problems
|
||||
- start with generic workspaces, suitable for common needs
|
||||
- more specific workspaces should be built on top of generic workspaces
|
||||
- rather than building ubder-workspace for all needs, workspaces should branch out from ther generic workspace and focus on specific needs
|
||||
- workspaces should be well-tested and documented
|
||||
- workspaces should include only open source tools
|
||||
- workspaces don't include as much stuff as possible, workspace should have minimal needed set of tools
|
||||
- include in the workspace only tools that share processes and data. Reverse proxy, load balanncers,
|
||||
databases, server resource monitors should not be the part of the workspace. Instead they should be integrated with
|
||||
workspace in docker compose.
|
||||
|
||||
## Contribution
|
||||
|
||||
Contributions, pull requests and any form of feedback or collaboration is super welcome!
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
11
workspaces/base-workspace/Cronicle-0.8.61/LICENSE.md
Executable file
|
@ -0,0 +1,11 @@
|
|||
# License
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 - 2018 Joseph Huckaby
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
3475
workspaces/base-workspace/Cronicle-0.8.61/README.md
Executable file
342
workspaces/base-workspace/Cronicle-0.8.61/bin/build-tools.js
Executable file
|
@ -0,0 +1,342 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Build Tools Module -- included by build.js.
|
||||
// Copies, symlinks and compresses files into the right locations.
|
||||
// Can also compact & bundle JS/CSS together for distribution.
|
||||
// Copyright (c) 2015 Joseph Huckaby, MIT License.
|
||||
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var zlib = require('zlib');
|
||||
var util = require('util');
|
||||
var os = require('os');
|
||||
var cp = require('child_process');
|
||||
|
||||
var mkdirp = require('mkdirp');
|
||||
var async = require('async');
|
||||
var glob = require('glob');
|
||||
var UglifyJS = require("uglify-js");
|
||||
var Tools = require('pixl-tools');
|
||||
|
||||
var fileStatSync = function(file) {
|
||||
// no-throw version of fs.statSync
|
||||
var stats = null;
|
||||
try { stats = fs.statSync(file); }
|
||||
catch (err) { return false; }
|
||||
return stats;
|
||||
};
|
||||
|
||||
var fileExistsSync = function(file) {
|
||||
// replacement for fs.existsSync which is being removed
|
||||
return !!fileStatSync(file);
|
||||
};
|
||||
|
||||
var symlinkFile = exports.symlinkFile = function symlinkFile( old_file, new_file, callback ) {
|
||||
// create symlink to file
|
||||
// Note: 'new_file' is the object that will be created on the filesystem
|
||||
// 'old_file' should already exist, and is the file being pointed to
|
||||
if (new_file.match(/\/$/)) new_file += path.basename(old_file);
|
||||
|
||||
// if target exists and is not a symlink, skip this
|
||||
try {
|
||||
var stats = fs.lstatSync(new_file);
|
||||
if (!stats.isSymbolicLink()) return callback();
|
||||
}
|
||||
catch (e) {;}
|
||||
|
||||
console.log( "Symlink: " + old_file + " --> " + new_file );
|
||||
|
||||
try { fs.unlinkSync( new_file ); }
|
||||
catch (e) {;}
|
||||
|
||||
if (!fileExistsSync( path.dirname(new_file) )) {
|
||||
mkdirp.sync( path.dirname(new_file) );
|
||||
}
|
||||
|
||||
// fs.symlink takes a STRING (not a file path per se) as the old (existing) file,
|
||||
// and it needs to be relative from new_file. So we need to resolve some things.
|
||||
var sym_new = path.resolve( new_file );
|
||||
var sym_old = path.relative( path.dirname(sym_new), path.resolve(old_file) );
|
||||
|
||||
fs.symlink( sym_old, sym_new, callback );
|
||||
};
|
||||
|
||||
var copyFile = exports.copyFile = function copyFile( old_file, new_file, callback ) {
|
||||
// copy file
|
||||
if (new_file.match(/\/$/)) new_file += path.basename(old_file);
|
||||
console.log( "Copy: " + old_file + " --> " + new_file );
|
||||
|
||||
try { fs.unlinkSync( new_file ); }
|
||||
catch (e) {;}
|
||||
|
||||
if (!fileExistsSync( path.dirname(new_file) )) {
|
||||
mkdirp.sync( path.dirname(new_file) );
|
||||
}
|
||||
|
||||
var inp = fs.createReadStream(old_file);
|
||||
var outp = fs.createWriteStream(new_file);
|
||||
inp.on('end', callback );
|
||||
inp.pipe( outp );
|
||||
};
|
||||
|
||||
var copyFiles = exports.copyFiles = function copyFiles( src_spec, dest_dir, callback ) {
|
||||
// copy multiple files to destination directory using filesystem globbing
|
||||
dest_dir = dest_dir.replace(/\/$/, '');
|
||||
|
||||
glob(src_spec, {}, function (err, files) {
|
||||
// got files
|
||||
if (files && files.length) {
|
||||
async.eachSeries( files, function(src_file, callback) {
|
||||
// foreach file
|
||||
var stats = fileStatSync(src_file);
|
||||
if (stats && stats.isFile()) {
|
||||
copyFile( src_file, dest_dir + '/', callback );
|
||||
}
|
||||
else callback();
|
||||
}, callback );
|
||||
} // got files
|
||||
else {
|
||||
callback( err || new Error("No files found matching: " + src_spec) );
|
||||
}
|
||||
} );
|
||||
};
|
||||
|
||||
var compressFile = exports.compressFile = function compressFile( src_file, gz_file, callback ) {
|
||||
// gzip compress file
|
||||
console.log( "Compress: " + src_file + " --> " + gz_file );
|
||||
|
||||
if (fileExistsSync(gz_file)) {
|
||||
fs.unlinkSync( gz_file );
|
||||
}
|
||||
|
||||
var gzip = zlib.createGzip();
|
||||
var inp = fs.createReadStream( src_file );
|
||||
var outp = fs.createWriteStream( gz_file );
|
||||
inp.on('end', callback );
|
||||
inp.pipe(gzip).pipe(outp);
|
||||
};
|
||||
|
||||
var copyCompress = exports.copyCompress = function copyCompress( old_file, new_file, callback ) {
|
||||
// copy file and create gzip version as well
|
||||
if (new_file.match(/\/$/)) new_file += path.basename(old_file);
|
||||
|
||||
copyFile( old_file, new_file, function(err) {
|
||||
if (err) return callback(err);
|
||||
|
||||
// Make a compressed copy, so node-static will serve it up to browsers
|
||||
compressFile( old_file, new_file + '.gz', callback );
|
||||
} );
|
||||
};
|
||||
|
||||
var symlinkCompress = exports.symlinkCompress = function symlinkCompress( old_file, new_file, callback ) {
|
||||
// symlink file and create gzip version as well
|
||||
if (new_file.match(/\/$/)) new_file += path.basename(old_file);
|
||||
|
||||
symlinkFile( old_file, new_file, function(err) {
|
||||
if (err) return callback(err);
|
||||
|
||||
// Make a compressed copy, so node-static will serve it up to browsers
|
||||
compressFile( old_file, new_file + '.gz', callback );
|
||||
} );
|
||||
};
|
||||
|
||||
var deleteFile = exports.deleteFile = function deleteFile( file, callback ) {
|
||||
// delete file
|
||||
console.log( "Delete: " + file );
|
||||
|
||||
if (fileExistsSync(file)) {
|
||||
fs.unlink( file, callback );
|
||||
}
|
||||
else callback();
|
||||
};
|
||||
|
||||
var deleteFiles = exports.deleteFiles = function deleteFiles( spec, callback ) {
|
||||
// delete multiple files using filesystem globbing
|
||||
glob(spec, {}, function (err, files) {
|
||||
// got files
|
||||
if (files && files.length) {
|
||||
async.eachSeries( files, function(file, callback) {
|
||||
// foreach file
|
||||
deleteFile( file, callback );
|
||||
}, callback );
|
||||
} // got files
|
||||
else {
|
||||
callback( err );
|
||||
}
|
||||
} );
|
||||
};
|
||||
|
||||
var chmodFiles = exports.chmodFiles = function chmodFiles( mode, spec, callback ) {
|
||||
// chmod multiple files to specified mode using filesystem globbing
|
||||
glob(spec, {}, function (err, files) {
|
||||
// got files
|
||||
if (files && files.length) {
|
||||
async.eachSeries( files, function(file, callback) {
|
||||
// foreach file
|
||||
fs.chmod( file, mode, callback );
|
||||
}, callback );
|
||||
} // got files
|
||||
else {
|
||||
callback( err || new Error("No files found matching: " + spec) );
|
||||
}
|
||||
} );
|
||||
};
|
||||
|
||||
var copyDir = exports.copyDir = function copyDir( src_dir, dest_dir, exclusive, callback ) {
|
||||
// recursively copy dir and contents, optionally with exclusive mode
|
||||
// symlinks are followed, and the target is copied instead
|
||||
var src_spec = src_dir + '/*';
|
||||
|
||||
// exclusive means skip if dest exists (do not replace)
|
||||
if (exclusive && fileExistsSync(dest_dir)) return callback();
|
||||
|
||||
mkdirp.sync( dest_dir );
|
||||
|
||||
glob(src_spec, {}, function (err, files) {
|
||||
// got files
|
||||
if (files && files.length) {
|
||||
async.eachSeries( files, function(src_file, callback) {
|
||||
// foreach file
|
||||
var stats = fs.statSync(src_file);
|
||||
|
||||
if (stats.isFile()) {
|
||||
copyFile( src_file, dest_dir + '/', callback );
|
||||
}
|
||||
else if (stats.isDirectory()) {
|
||||
copyDir( src_file, dest_dir + '/' + path.basename(src_file), exclusive, callback );
|
||||
}
|
||||
}, callback );
|
||||
} // got files
|
||||
else {
|
||||
callback( err );
|
||||
}
|
||||
} );
|
||||
};
|
||||
|
||||
var bundleCompress = exports.bundleCompress = function bundleCompress( args, callback ) {
|
||||
// compress bundle of files
|
||||
var html_file = args.html_file;
|
||||
var html_dir = path.dirname( html_file );
|
||||
|
||||
if (!fileExistsSync( path.dirname(args.dest_bundle) )) {
|
||||
mkdirp.sync( path.dirname(args.dest_bundle) );
|
||||
}
|
||||
|
||||
var raw_html = fs.readFileSync( html_file, 'utf8' );
|
||||
var regexp = new RegExp( "\\<\\!\\-\\-\\s+BUILD\\:\\s*"+args.match_key+"_START\\s*\\-\\-\\>([^]+)\\<\\!\\-\\-\\s*BUILD\\:\\s+"+args.match_key+"_END\\s*\\-\\-\\>" );
|
||||
|
||||
if (raw_html.match(regexp)) {
|
||||
var files_raw = RegExp.$1;
|
||||
var files = [];
|
||||
|
||||
files_raw.replace( /\b(src|href)\=[\"\']([^\"\']+)[\"\']/ig, function(m_all, m_g1, m_g2) {
|
||||
files.push( path.join(html_dir, m_g2) );
|
||||
} );
|
||||
|
||||
if (files.length) {
|
||||
console.log("Bundling files: ", files);
|
||||
var raw_output = '';
|
||||
|
||||
// optionally add file header
|
||||
if (args.header) {
|
||||
raw_output += args.header.trim() + "\n";
|
||||
}
|
||||
|
||||
if (args.uglify) {
|
||||
console.log("Running UglifyJS...");
|
||||
var result = UglifyJS.minify( files );
|
||||
if (!result || !result.code) return callback( new Error("Failed to bundle script: Uglify failure") );
|
||||
raw_output += result.code;
|
||||
}
|
||||
else {
|
||||
for (var idx = 0, len = files.length; idx < len; idx++) {
|
||||
var file = files[idx];
|
||||
raw_output += fs.readFileSync( file, 'utf8' ) + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// optionally strip source map links
|
||||
// /*# sourceMappingURL=materialdesignicons.min.css.map */
|
||||
if (args.strip_source_maps) {
|
||||
raw_output = raw_output.replace(/sourceMappingURL\=\S+/g, "");
|
||||
}
|
||||
|
||||
// write out our bundle
|
||||
console.log(" --> " + args.dest_bundle);
|
||||
fs.writeFileSync(args.dest_bundle, raw_output);
|
||||
|
||||
// swap a ref link into a copy of the HTML
|
||||
console.log(" --> " + html_file );
|
||||
raw_html = raw_html.replace( regexp, args.dest_bundle_tag );
|
||||
fs.writeFileSync(html_file, raw_html);
|
||||
|
||||
// now make a compressed version of the bundle
|
||||
compressFile( args.dest_bundle, args.dest_bundle + '.gz', function(err) {
|
||||
if (err) return callback(err);
|
||||
|
||||
// and compress the final HTML as well
|
||||
compressFile( html_file, html_file + '.gz', callback );
|
||||
});
|
||||
|
||||
} // found files
|
||||
else {
|
||||
callback( new Error("Could not locate any file references: " + args.src_html + ": " + args.match_key) );
|
||||
}
|
||||
}
|
||||
else {
|
||||
callback( new Error("Could not locate match in HTML source: " + args.src_html + ": " + args.match_key) );
|
||||
}
|
||||
};
|
||||
|
||||
var generateSecretKey = exports.generateSecretKey = function generateSecretKey( args, callback ) {
|
||||
// generate random secret key for a specified JSON config file
|
||||
// use regular expression to preserve natural file format
|
||||
var file = args.file;
|
||||
var key = args.key;
|
||||
var regex = new RegExp('(\\"'+Tools.escapeRegExp(key)+'\\"\\s*\\:\\s*\\")(.*?)(\\")');
|
||||
var secret_key = Tools.generateUniqueID(32);
|
||||
|
||||
fs.readFile(file, 'utf8', function(err, text) {
|
||||
if (err) return callback(err);
|
||||
if (!text.match(regex)) return callback( new Error("Could not locate key to replace: " + file + ": " + key) );
|
||||
|
||||
text = text.replace(regex, '$1' + secret_key + '$3');
|
||||
fs.writeFile( file, text, callback );
|
||||
});
|
||||
};
|
||||
|
||||
var addToServerStartup = exports.addToServerStartup = function addToServerStartup( args, callback ) {
|
||||
// add service to init.d
|
||||
// (RedHat and Ubuntu only -- do nothing on OS X)
|
||||
var src_file = args.src_file;
|
||||
var dest_dir = args.dest_dir;
|
||||
var service_name = args.service_name;
|
||||
var dest_file = dest_dir + '/' + service_name;
|
||||
|
||||
// shell command that activates service on redhat or ubuntu
|
||||
var cmd = 'chkconfig '+service_name+' on || update-rc.d '+service_name+' defaults';
|
||||
|
||||
// skip on os x, and if init dir is missing
|
||||
if (os.platform() == 'darwin') return callback();
|
||||
if (!fileExistsSync(dest_dir)) return callback( new Error("Cannot locate init.d directory: " + dest_dir) );
|
||||
if (process.getuid() != 0) return callback( new Error("Must be root to add services to server startup system.") );
|
||||
|
||||
// copy file into place
|
||||
copyFile( src_file, dest_file, function(err) {
|
||||
if (err) return callback(err);
|
||||
|
||||
// must be executable
|
||||
fs.chmod( dest_file, "775", function(err) {
|
||||
if (err) return callback(err);
|
||||
|
||||
// exec shell command
|
||||
cp.exec( cmd, callback );
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
||||
var printMessage = exports.printMessage = function printMessage( args, callback ) {
|
||||
// output a message to the console
|
||||
// use process.stdout.write because console.log has been redirected to a file.
|
||||
process.stdout.write( "\n" + args.lines.join("\n") + "\n\n" );
|
||||
};
|
71
workspaces/base-workspace/Cronicle-0.8.61/bin/build.js
Executable file
|
@ -0,0 +1,71 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Simple Node Project Builder
|
||||
// Copies, symlinks and compresses files into the right locations.
|
||||
// Can also compact & bundle JS/CSS together for distribution.
|
||||
// Copyright (c) 2015 Joseph Huckaby, MIT License.
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var util = require('util');
|
||||
var async = require('async');
|
||||
var mkdirp = require('mkdirp');
|
||||
|
||||
var BuildTools = require('./build-tools.js');
|
||||
var setup = require('../sample_conf/setup.json');
|
||||
|
||||
var mode = 'dist';
|
||||
if (process.argv.length > 2) mode = process.argv[2];
|
||||
|
||||
var steps = setup.build.common || [];
|
||||
if (setup.build[mode]) {
|
||||
steps = steps.concat( setup.build[mode] );
|
||||
}
|
||||
|
||||
// chdir to the proper server root dir
|
||||
process.chdir( path.dirname( __dirname ) );
|
||||
|
||||
// make sure we have a logs dir
|
||||
mkdirp.sync( 'logs' );
|
||||
fs.chmodSync( 'logs', "755" );
|
||||
|
||||
// log to file instead of console
|
||||
console.log = function(msg, data) {
|
||||
if (data) msg += ' ' + JSON.stringify(data);
|
||||
fs.appendFile( 'logs/install.log', msg + "\n", function() {} );
|
||||
};
|
||||
|
||||
console.log("\nBuilding project ("+mode+")...\n");
|
||||
|
||||
async.eachSeries( steps, function(step, callback) {
|
||||
// foreach step
|
||||
|
||||
// util.isArray is DEPRECATED??? Nooooooooode!
|
||||
var isArray = Array.isArray || util.isArray;
|
||||
|
||||
if (isArray(step)) {
|
||||
// [ "symlinkFile", "node_modules/pixl-webapp/js", "htdocs/js/common" ],
|
||||
var func = step.shift();
|
||||
console.log( func + ": " + JSON.stringify(step));
|
||||
step.push( callback );
|
||||
BuildTools[func].apply( null, step );
|
||||
}
|
||||
else {
|
||||
// { "action": "bundleCompress", ... }
|
||||
var func = step.action;
|
||||
delete step.action;
|
||||
console.log( func + ": " + JSON.stringify(step));
|
||||
BuildTools[func].apply( null, [step, callback] );
|
||||
}
|
||||
},
|
||||
function(err) {
|
||||
// done with iteration, check for error
|
||||
if (err) {
|
||||
console.error("\nBuild Error: " + err + "\n");
|
||||
}
|
||||
else {
|
||||
console.log("\nBuild complete.\n");
|
||||
}
|
||||
} );
|
||||
|
||||
// End
|
219
workspaces/base-workspace/Cronicle-0.8.61/bin/control.sh
Executable file
|
@ -0,0 +1,219 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# Control script designed to allow an easy command line interface
|
||||
# to controlling any binary. Written by Marc Slemko, 1997/08/23
|
||||
# Modified for Cronicle, Joe Huckaby, 2015/08/11
|
||||
#
|
||||
# The exit codes returned are:
|
||||
# 0 - operation completed successfully
|
||||
# 2 - usage error
|
||||
# 3 - binary could not be started
|
||||
# 4 - binary could not be stopped
|
||||
# 8 - configuration syntax error
|
||||
#
|
||||
# When multiple arguments are given, only the error from the _last_
|
||||
# one is reported. Run "*ctl help" for usage info
|
||||
#
|
||||
#
|
||||
# |||||||||||||||||||| START CONFIGURATION SECTION ||||||||||||||||||||
|
||||
# -------------------- --------------------
|
||||
#
|
||||
# the name of your binary
|
||||
NAME="Cronicle Daemon"
|
||||
#
|
||||
# home directory
|
||||
HOMEDIR="$(dirname "$(cd -- "$(dirname "$0")" && (pwd -P 2>/dev/null || pwd))")"
|
||||
cd $HOMEDIR
|
||||
#
|
||||
# the path to your binary, including options if necessary
|
||||
BINARY="node $HOMEDIR/lib/main.js"
|
||||
#
|
||||
# the path to your PID file
|
||||
PIDFILE=$HOMEDIR/logs/cronicled.pid
|
||||
#
|
||||
# -------------------- --------------------
|
||||
# |||||||||||||||||||| END CONFIGURATION SECTION ||||||||||||||||||||
|
||||
|
||||
ERROR=0
|
||||
ARGV="$@"
|
||||
if [ "x$ARGV" = "x" ] ; then
|
||||
ARGS="help"
|
||||
fi
|
||||
|
||||
for ARG in $@ $ARGS
|
||||
do
|
||||
# check for pidfile
|
||||
if [ -f $PIDFILE ] ; then
|
||||
PID=`cat $PIDFILE`
|
||||
if [ "x$PID" != "x" ] && kill -0 $PID 2>/dev/null ; then
|
||||
STATUS="$NAME running (pid $PID)"
|
||||
RUNNING=1
|
||||
else
|
||||
STATUS="$NAME not running (pid $PID?)"
|
||||
RUNNING=0
|
||||
fi
|
||||
else
|
||||
STATUS="$NAME not running (no pid file)"
|
||||
RUNNING=0
|
||||
fi
|
||||
|
||||
case $ARG in
|
||||
start)
|
||||
if [ $RUNNING -eq 1 ]; then
|
||||
echo "$ARG: $NAME already running (pid $PID)"
|
||||
continue
|
||||
fi
|
||||
echo "$0 $ARG: Starting up $NAME..."
|
||||
if $BINARY ; then
|
||||
echo "$0 $ARG: $NAME started"
|
||||
else
|
||||
echo "$0 $ARG: $NAME could not be started"
|
||||
ERROR=3
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
if [ $RUNNING -eq 0 ]; then
|
||||
echo "$ARG: $STATUS"
|
||||
continue
|
||||
fi
|
||||
if kill $PID ; then
|
||||
while [ "x$PID" != "x" ] && kill -0 $PID 2>/dev/null ; do
|
||||
sleep 1;
|
||||
done
|
||||
echo "$0 $ARG: $NAME stopped"
|
||||
else
|
||||
echo "$0 $ARG: $NAME could not be stopped"
|
||||
ERROR=4
|
||||
fi
|
||||
;;
|
||||
restart)
|
||||
$0 stop start
|
||||
;;
|
||||
cycle)
|
||||
$0 stop start
|
||||
;;
|
||||
status)
|
||||
echo "$ARG: $STATUS"
|
||||
;;
|
||||
setup)
|
||||
node $HOMEDIR/bin/storage-cli.js setup
|
||||
exit
|
||||
;;
|
||||
maint)
|
||||
node $HOMEDIR/bin/storage-cli.js maint $2
|
||||
exit
|
||||
;;
|
||||
admin)
|
||||
node $HOMEDIR/bin/storage-cli.js admin $2 $3
|
||||
exit
|
||||
;;
|
||||
export)
|
||||
node $HOMEDIR/bin/storage-cli.js export $2 $3 $4
|
||||
exit
|
||||
;;
|
||||
import)
|
||||
if [ $RUNNING -eq 1 ]; then
|
||||
$0 stop
|
||||
fi
|
||||
node $HOMEDIR/bin/storage-cli.js import $2 $3 $4
|
||||
exit
|
||||
;;
|
||||
upgrade)
|
||||
node $HOMEDIR/bin/install.js $2 || exit 1
|
||||
exit
|
||||
;;
|
||||
migrate)
|
||||
node $HOMEDIR/bin/storage-migrate.js $2 $3 $4
|
||||
exit
|
||||
;;
|
||||
version)
|
||||
PACKAGE_VERSION=$(node -p -e "require('./package.json').version")
|
||||
echo "$PACKAGE_VERSION"
|
||||
exit
|
||||
;;
|
||||
*)
|
||||
echo "usage: $0 (start|stop|cycle|status|setup|maint|admin|export|import|upgrade|help)"
|
||||
cat <<EOF
|
||||
|
||||
start - Starts $NAME.
|
||||
stop - Stops $NAME and wait until it actually exits.
|
||||
restart - Calls stop, then start (hard restart).
|
||||
status - Checks whether $NAME is currently running.
|
||||
setup - Runs initial storage setup.
|
||||
maint - Runs daily maintenance routine.
|
||||
admin - Creates new emergency admin account (specify user / pass).
|
||||
export - Exports data to specified file.
|
||||
import - Imports data from specified file.
|
||||
upgrade - Upgrades $NAME to the latest stable (or specify version).
|
||||
migrate - Migrate storage data to another location.
|
||||
version - Outputs the current $NAME package version.
|
||||
help - Displays this screen.
|
||||
|
||||
EOF
|
||||
ERROR=2
|
||||
;;
|
||||
|
||||
esac
|
||||
|
||||
done
|
||||
|
||||
exit $ERROR
|
||||
|
||||
## ====================================================================
|
||||
## The Apache Software License, Version 1.1
|
||||
##
|
||||
## Copyright (c) 2000 The Apache Software Foundation. All rights
|
||||
## reserved.
|
||||
##
|
||||
## Redistribution and use in source and binary forms, with or without
|
||||
## modification, are permitted provided that the following conditions
|
||||
## are met:
|
||||
##
|
||||
## 1. Redistributions of source code must retain the above copyright
|
||||
## notice, this list of conditions and the following disclaimer.
|
||||
##
|
||||
## 2. Redistributions in binary form must reproduce the above copyright
|
||||
## notice, this list of conditions and the following disclaimer in
|
||||
## the documentation and/or other materials provided with the
|
||||
## distribution.
|
||||
##
|
||||
## 3. The end-user documentation included with the redistribution,
|
||||
## if any, must include the following acknowledgment:
|
||||
## "This product includes software developed by the
|
||||
## Apache Software Foundation (http://www.apache.org/)."
|
||||
## Alternately, this acknowledgment may appear in the software itself,
|
||||
## if and wherever such third-party acknowledgments normally appear.
|
||||
##
|
||||
## 4. The names "Apache" and "Apache Software Foundation" must
|
||||
## not be used to endorse or promote products derived from this
|
||||
## software without prior written permission. For written
|
||||
## permission, please contact apache@apache.org.
|
||||
##
|
||||
## 5. Products derived from this software may not be called "Apache",
|
||||
## nor may "Apache" appear in their name, without prior written
|
||||
## permission of the Apache Software Foundation.
|
||||
##
|
||||
## THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
|
||||
## WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
## OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
## DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
|
||||
## ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
||||
## USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
## OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
|
||||
## OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
## SUCH DAMAGE.
|
||||
## ====================================================================
|
||||
##
|
||||
## This software consists of voluntary contributions made by many
|
||||
## individuals on behalf of the Apache Software Foundation. For more
|
||||
## information on the Apache Software Foundation, please see
|
||||
## <http://www.apache.org/>.
|
||||
##
|
||||
## Portions of this software are based upon public domain software
|
||||
## originally written at the National Center for Supercomputing Applications,
|
||||
## University of Illinois, Urbana-Champaign.
|
||||
##
|
||||
#
|
20
workspaces/base-workspace/Cronicle-0.8.61/bin/cronicled.init
Executable file
|
@ -0,0 +1,20 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# init.d script for Cronicle Scheduler
|
||||
#
|
||||
# chkconfig: 345 90 10
|
||||
# description: Cronicle Scheduler
|
||||
|
||||
### BEGIN INIT INFO
|
||||
# Provides: cronicled
|
||||
# Required-Start: $local_fs $remote_fs $network $syslog $named
|
||||
# Required-Stop: $local_fs $remote_fs $network $syslog $named
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# X-Interactive: true
|
||||
# Short-Description: Start/Stop Cronicle Scheduler
|
||||
### END INIT INFO
|
||||
|
||||
PATH=/sbin:/bin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
|
||||
|
||||
/opt/cronicle/bin/control.sh $1
|
10
workspaces/base-workspace/Cronicle-0.8.61/bin/debug.sh
Executable file
|
@ -0,0 +1,10 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Start Cronicle in debug mode
|
||||
# No daemon fork, and all logs emitted to stdout
|
||||
# Add --master to force instant master on startup
|
||||
|
||||
HOMEDIR="$(dirname "$(cd -- "$(dirname "$0")" && (pwd -P 2>/dev/null || pwd))")"
|
||||
|
||||
cd $HOMEDIR
|
||||
node --trace-warnings $HOMEDIR/lib/main.js --debug --echo "$@"
|
255
workspaces/base-workspace/Cronicle-0.8.61/bin/install.js
Executable file
|
@ -0,0 +1,255 @@
|
|||
// Cronicle Auto Installer
|
||||
// Copyright (c) 2015 - 2019 Joseph Huckaby, MIT License.
|
||||
// https://github.com/jhuckaby/Cronicle
|
||||
|
||||
// To install, issue this command as root:
|
||||
// curl -s "https://raw.githubusercontent.com/jhuckaby/Cronicle/master/bin/install.js" | node
|
||||
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var util = require('util');
|
||||
var os = require('os');
|
||||
var cp = require('child_process');
|
||||
|
||||
var installer_version = '1.3';
|
||||
var base_dir = '/opt/cronicle';
|
||||
var log_dir = base_dir + '/logs';
|
||||
var log_file = '';
|
||||
var gh_repo_url = 'http://github.com/jhuckaby/Cronicle';
|
||||
var gh_releases_url = 'https://api.github.com/repos/jhuckaby/Cronicle/releases';
|
||||
var gh_head_tarball_url = 'https://github.com/jhuckaby/Cronicle/archive/master.tar.gz';
|
||||
|
||||
// don't allow npm to delete these (ugh)
|
||||
var packages_to_check = ['couchbase', 'aws-sdk', 'redis'];
|
||||
var packages_to_rescue = {};
|
||||
|
||||
var restore_packages = function() {
|
||||
// restore packages that npm killed during upgrade
|
||||
var cmd = "npm install";
|
||||
for (var pkg in packages_to_rescue) {
|
||||
cmd += ' ' + pkg + '@' + packages_to_rescue[pkg];
|
||||
}
|
||||
if (log_file) {
|
||||
fs.appendFileSync(log_file, "\nExecuting npm command to restore lost packages: " + cmd + "\n");
|
||||
cmd += ' >>' + log_file + ' 2>&1';
|
||||
}
|
||||
cp.execSync(cmd);
|
||||
};
|
||||
|
||||
var print = function(msg) {
|
||||
process.stdout.write(msg);
|
||||
if (log_file) fs.appendFileSync(log_file, msg);
|
||||
};
|
||||
var warn = function(msg) {
|
||||
process.stderr.write(msg);
|
||||
if (log_file) fs.appendFileSync(log_file, msg);
|
||||
};
|
||||
var die = function(msg) {
|
||||
warn( "\nERROR: " + msg.trim() + "\n\n" );
|
||||
process.exit(1);
|
||||
};
|
||||
var logonly = function(msg) {
|
||||
if (log_file) fs.appendFileSync(log_file, msg);
|
||||
};
|
||||
|
||||
if (process.getuid() != 0) {
|
||||
die( "The Cronicle auto-installer must be run as root." );
|
||||
}
|
||||
|
||||
// create base and log directories
|
||||
try { cp.execSync( "mkdir -p " + base_dir + " && chmod 775 " + base_dir ); }
|
||||
catch (err) { die("Failed to create base directory: " + base_dir + ": " + err); }
|
||||
|
||||
try { cp.execSync( "mkdir -p " + log_dir + " && chmod 777 " + log_dir ); }
|
||||
catch (err) { die("Failed to create log directory: " + log_dir + ": " + err); }
|
||||
|
||||
// start logging from this point onward
|
||||
log_file = log_dir + '/install.log';
|
||||
logonly( "\nStarting install run: " + (new Date()).toString() + "\n" );
|
||||
|
||||
print(
|
||||
"\nCronicle Installer v" + installer_version + "\n" +
|
||||
"Copyright (c) 2015 - 2018 PixlCore.com. MIT Licensed.\n" +
|
||||
"Log File: " + log_file + "\n\n"
|
||||
);
|
||||
|
||||
process.chdir( base_dir );
|
||||
|
||||
var is_preinstalled = false;
|
||||
var cur_version = '';
|
||||
var new_version = process.argv[2] || '';
|
||||
|
||||
try {
|
||||
var stats = fs.statSync( base_dir + '/package.json' );
|
||||
var json = require( base_dir + '/package.json' );
|
||||
if (json && json.version) {
|
||||
cur_version = json.version;
|
||||
is_preinstalled = true;
|
||||
}
|
||||
}
|
||||
catch (err) {;}
|
||||
|
||||
var is_running = false;
|
||||
if (is_preinstalled) {
|
||||
var pid_file = log_dir + '/cronicled.pid';
|
||||
try {
|
||||
var pid = fs.readFileSync(pid_file, { encoding: 'utf8' });
|
||||
is_running = process.kill( pid, 0 );
|
||||
}
|
||||
catch (err) {;}
|
||||
}
|
||||
|
||||
print( "Fetching release list...\n");
|
||||
logonly( "Releases URL: " + gh_releases_url + "\n" );
|
||||
|
||||
cp.exec('curl -s ' + gh_releases_url, function (err, stdout, stderr) {
|
||||
if (err) {
|
||||
print( stdout.toString() );
|
||||
warn( stderr.toString() );
|
||||
die("Failed to fetch release list: " + gh_releases_url + ": " + err);
|
||||
}
|
||||
|
||||
var releases = null;
|
||||
try { releases = JSON.parse( stdout.toString() ); }
|
||||
catch (err) {
|
||||
die("Failed to parse JSON from GitHub: " + gh_releases_url + ": " + err);
|
||||
}
|
||||
|
||||
// util.isArray is DEPRECATED??? Nooooooooode!
|
||||
var isArray = Array.isArray || util.isArray;
|
||||
if (!isArray(releases)) die("Unexpected response from GitHub Releases API: " + gh_releases_url + ": Not an array");
|
||||
|
||||
var release = null;
|
||||
for (var idx = 0, len = releases.length; idx < len; idx++) {
|
||||
var rel = releases[idx];
|
||||
var ver = rel.tag_name.replace(/^\D+/, '');
|
||||
rel.version = ver;
|
||||
|
||||
if (!new_version || (ver == new_version)) {
|
||||
release = rel;
|
||||
new_version = ver;
|
||||
idx = len;
|
||||
}
|
||||
} // foreach release
|
||||
|
||||
if (!release) {
|
||||
// no release found -- use HEAD rev?
|
||||
if (!new_version || new_version.match(/HEAD/i)) {
|
||||
release = {
|
||||
version: 'HEAD',
|
||||
tarball_url: gh_head_tarball_url
|
||||
};
|
||||
}
|
||||
else {
|
||||
die("Release not found: " + new_version);
|
||||
}
|
||||
}
|
||||
|
||||
// sanity check
|
||||
if (is_preinstalled && (cur_version == new_version)) {
|
||||
if (process.argv[2]) print( "\nVersion " + cur_version + " is already installed.\n\n" );
|
||||
else print( "\nVersion " + cur_version + " is already installed, and is the latest.\n\n" );
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// proceed with installation
|
||||
if (is_preinstalled) print("Upgrading Cronicle from v"+cur_version+" to v"+new_version+"...\n");
|
||||
else print("Installing Cronicle v"+new_version+"...\n");
|
||||
|
||||
if (is_running) {
|
||||
print("\n");
|
||||
try { cp.execSync( base_dir + "/bin/control.sh stop", { stdio: 'inherit' } ); }
|
||||
catch (err) { die("Failed to stop Cronicle: " + err); }
|
||||
print("\n");
|
||||
}
|
||||
|
||||
// download tarball and expand into current directory
|
||||
var tarball_url = release.tarball_url;
|
||||
logonly( "Tarball URL: " + tarball_url + "\n" );
|
||||
|
||||
cp.exec('curl -L ' + tarball_url + ' | tar zxf - --strip-components 1', function (err, stdout, stderr) {
|
||||
if (err) {
|
||||
print( stdout.toString() );
|
||||
warn( stderr.toString() );
|
||||
die("Failed to download release: " + tarball_url + ": " + err);
|
||||
}
|
||||
else {
|
||||
logonly( stdout.toString() + stderr.toString() );
|
||||
}
|
||||
|
||||
try {
|
||||
var stats = fs.statSync( base_dir + '/package.json' );
|
||||
var json = require( base_dir + '/package.json' );
|
||||
}
|
||||
catch (err) {
|
||||
die("Failed to download package: " + tarball_url + ": " + err);
|
||||
}
|
||||
|
||||
print( is_preinstalled ? "Updating dependencies...\n" : "Installing dependencies...\n");
|
||||
|
||||
var npm_cmd = is_preinstalled ? "npm update --unsafe-perm" : "npm install --unsafe-perm";
|
||||
logonly( "Executing command: " + npm_cmd + "\n" );
|
||||
|
||||
// temporarily stash add-on modules that were installed separately (thanks npm)
|
||||
if (is_preinstalled) packages_to_check.forEach( function(pkg) {
|
||||
if (fs.existsSync('node_modules/' + pkg)) {
|
||||
packages_to_rescue[pkg] = JSON.parse( fs.readFileSync('node_modules/' + pkg + '/package.json', 'utf8') ).version;
|
||||
}
|
||||
});
|
||||
|
||||
// install dependencies via npm
|
||||
cp.exec(npm_cmd, function (err, stdout, stderr) {
|
||||
if (err) {
|
||||
print( stdout.toString() );
|
||||
warn( stderr.toString() );
|
||||
if (is_preinstalled) restore_packages();
|
||||
die("Failed to install dependencies: " + err);
|
||||
}
|
||||
else {
|
||||
logonly( stdout.toString() + stderr.toString() );
|
||||
}
|
||||
|
||||
print("Running post-install script...\n");
|
||||
logonly( "Executing command: node bin/build.js dist\n" );
|
||||
|
||||
// finally, run postinstall script
|
||||
cp.exec('node bin/build.js dist', function (err, stdout, stderr) {
|
||||
if (is_preinstalled) {
|
||||
// for upgrades only print output on error
|
||||
if (err) {
|
||||
print( stdout.toString() );
|
||||
warn( stderr.toString() );
|
||||
if (is_preinstalled) restore_packages();
|
||||
die("Failed to run post-install: " + err);
|
||||
}
|
||||
else {
|
||||
if (is_preinstalled) restore_packages();
|
||||
print("Upgrade complete.\n\n");
|
||||
|
||||
if (is_running) {
|
||||
try { cp.execSync( base_dir + "/bin/control.sh start", { stdio: 'inherit' } ); }
|
||||
catch (err) { die("Failed to start Cronicle: " + err); }
|
||||
print("\n");
|
||||
}
|
||||
}
|
||||
} // upgrade
|
||||
else {
|
||||
// first time install, always print output
|
||||
print( stdout.toString() );
|
||||
warn( stderr.toString() );
|
||||
|
||||
if (err) {
|
||||
die("Failed to run post-install: " + err);
|
||||
}
|
||||
else {
|
||||
print("Installation complete.\n\n");
|
||||
}
|
||||
} // first install
|
||||
|
||||
logonly( "Completed install run: " + (new Date()).toString() + "\n" );
|
||||
|
||||
process.exit(0);
|
||||
} ); // build.js
|
||||
} ); // npm
|
||||
} ); // download
|
||||
} ); // releases api
|
142
workspaces/base-workspace/Cronicle-0.8.61/bin/run-detached.js
Executable file
|
@ -0,0 +1,142 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Detached Plugin Runner for Cronicle
|
||||
// Copyright (c) 2015 - 2017 Joseph Huckaby
|
||||
// Released under the MIT License
|
||||
|
||||
var fs = require('fs');
|
||||
var os = require('os');
|
||||
var cp = require('child_process');
|
||||
var path = require('path');
|
||||
var sqparse = require('shell-quote').parse;
|
||||
var JSONStream = require('pixl-json-stream');
|
||||
var Tools = require('pixl-tools');
|
||||
|
||||
var args = process.argv.slice(-2);
|
||||
if (!args[1] || !args[1].match(/\.json$/)) {
|
||||
throw new Error("Usage: ./run-detached.js detached /PATH/TO/JSON/FILE.json");
|
||||
}
|
||||
|
||||
var job_file = args[1];
|
||||
var job = require( job_file );
|
||||
fs.unlink( job_file, function(err) {;} );
|
||||
|
||||
var child_cmd = job.command;
|
||||
var child_args = [];
|
||||
|
||||
// if command has cli args, parse using shell-quote
|
||||
if (child_cmd.match(/\s+(.+)$/)) {
|
||||
var cargs_raw = RegExp.$1;
|
||||
child_cmd = child_cmd.replace(/\s+(.+)$/, '');
|
||||
child_args = sqparse( cargs_raw, process.env );
|
||||
}
|
||||
|
||||
var child = cp.spawn( child_cmd, child_args, {
|
||||
stdio: ['pipe', 'pipe', fs.openSync(job.log_file, 'a')]
|
||||
} );
|
||||
|
||||
var updates = { detached_pid: child.pid };
|
||||
var kill_timer = null;
|
||||
var update_timer = null;
|
||||
|
||||
var cstream = new JSONStream( child.stdout, child.stdin );
|
||||
cstream.recordRegExp = /^\s*\{.+\}\s*$/;
|
||||
|
||||
cstream.on('json', function(data) {
|
||||
// received JSON data from child
|
||||
// store in object and send to Cronicle on exit
|
||||
for (var key in data) updates[key] = data[key];
|
||||
} );
|
||||
|
||||
cstream.on('text', function(line) {
|
||||
// received non-json text from child, just log it
|
||||
fs.appendFileSync(job.log_file, line);
|
||||
} );
|
||||
|
||||
cstream.on('error', function(err, text) {
|
||||
// Probably a JSON parse error (child emitting garbage)
|
||||
if (text) fs.appendFileSync(job.log_file, text + "\n");
|
||||
} );
|
||||
|
||||
child.on('error', function (err) {
|
||||
// child error
|
||||
updates = {};
|
||||
if (kill_timer) clearTimeout(kill_timer);
|
||||
if (update_timer) clearTimeout(update_timer);
|
||||
|
||||
var queue_file = job.queue_dir + '/' + job.id + '-' + Date.now() + '.json';
|
||||
fs.writeFileSync( queue_file + '.tmp', JSON.stringify({
|
||||
action: "detachedJobUpdate",
|
||||
id: job.id,
|
||||
complete: 1,
|
||||
code: 1,
|
||||
description: "Script failed: " + Tools.getErrorDescription(err)
|
||||
}) );
|
||||
fs.renameSync( queue_file + '.tmp', queue_file );
|
||||
} );
|
||||
|
||||
child.on('exit', function (code, signal) {
|
||||
// child exited
|
||||
if (kill_timer) clearTimeout(kill_timer);
|
||||
if (update_timer) clearTimeout(update_timer);
|
||||
|
||||
code = (code || signal || 0);
|
||||
if (code && !updates.code) {
|
||||
updates.code = code;
|
||||
updates.description = "Plugin exited with code: " + code;
|
||||
}
|
||||
|
||||
updates.action = "detachedJobUpdate";
|
||||
updates.id = job.id;
|
||||
updates.complete = 1;
|
||||
updates.time_end = Tools.timeNow();
|
||||
|
||||
// write file atomically, just in case
|
||||
var queue_file = job.queue_dir + '/' + job.id + '-complete.json';
|
||||
fs.writeFileSync( queue_file + '.tmp', JSON.stringify(updates) );
|
||||
fs.renameSync( queue_file + '.tmp', queue_file );
|
||||
} );
|
||||
|
||||
// silence EPIPE errors on child STDIN
|
||||
child.stdin.on('error', function(err) {
|
||||
// ignore
|
||||
} );
|
||||
|
||||
// send initial job + params
|
||||
cstream.write( job );
|
||||
|
||||
// we're done writing to the child -- don't hold open its stdin
|
||||
child.stdin.end();
|
||||
|
||||
// send updates every N seconds, if the child sent us anything (i.e. progress updates)
|
||||
// randomize interval so we don't bash the queue dir when multiple detached jobs are running
|
||||
update_timer = setInterval( function() {
|
||||
if (Tools.numKeys(updates) && !updates.complete) {
|
||||
|
||||
updates.action = "detachedJobUpdate";
|
||||
updates.id = job.id;
|
||||
updates.in_progress = 1;
|
||||
|
||||
// write file atomically, just in case
|
||||
var queue_file = job.queue_dir + '/' + job.id + '-' + Date.now() + '.json';
|
||||
fs.writeFileSync( queue_file + '.tmp', JSON.stringify(updates) );
|
||||
fs.renameSync( queue_file + '.tmp', queue_file );
|
||||
|
||||
updates = {};
|
||||
}
|
||||
}, 30000 + Math.floor( Math.random() * 25000 ) );
|
||||
|
||||
// Handle termination (server shutdown or job aborted)
|
||||
process.on('SIGTERM', function() {
|
||||
// console.log("Caught SIGTERM, killing child: " + child.pid);
|
||||
if (update_timer) clearTimeout(update_timer);
|
||||
|
||||
kill_timer = setTimeout( function() {
|
||||
// child didn't die, kill with prejudice
|
||||
// console.log("Child did not exit, killing harder: " + child.pid);
|
||||
child.kill('SIGKILL');
|
||||
}, 9 * 1000 );
|
||||
|
||||
// try killing nicely first
|
||||
child.kill('SIGTERM');
|
||||
} );
|
137
workspaces/base-workspace/Cronicle-0.8.61/bin/shell-plugin.js
Executable file
|
@ -0,0 +1,137 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Shell Script Runner for Cronicle
|
||||
// Invoked via the 'Shell Script' Plugin
|
||||
// Copyright (c) 2015 Joseph Huckaby
|
||||
// Released under the MIT License
|
||||
|
||||
var fs = require('fs');
|
||||
var os = require('os');
|
||||
var cp = require('child_process');
|
||||
var path = require('path');
|
||||
var JSONStream = require('pixl-json-stream');
|
||||
var Tools = require('pixl-tools');
|
||||
|
||||
// setup stdin / stdout streams
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdout.setEncoding('utf8');
|
||||
|
||||
var stream = new JSONStream( process.stdin, process.stdout );
|
||||
stream.on('json', function(job) {
|
||||
// got job from parent
|
||||
var script_file = path.join( os.tmpdir(), 'cronicle-script-temp-' + job.id + '.sh' );
|
||||
fs.writeFileSync( script_file, job.params.script, { mode: "775" } );
|
||||
|
||||
var child = cp.spawn( script_file, [], {
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
} );
|
||||
|
||||
var kill_timer = null;
|
||||
var stderr_buffer = '';
|
||||
|
||||
var cstream = new JSONStream( child.stdout, child.stdin );
|
||||
cstream.recordRegExp = /^\s*\{.+\}\s*$/;
|
||||
|
||||
cstream.on('json', function(data) {
|
||||
// received JSON data from child, pass along to Cronicle or log
|
||||
if (job.params.json) stream.write(data);
|
||||
else cstream.emit('text', JSON.stringify(data) + "\n");
|
||||
} );
|
||||
|
||||
cstream.on('text', function(line) {
|
||||
// received non-json text from child
|
||||
// look for plain number from 0 to 100, treat as progress update
|
||||
if (line.match(/^\s*(\d+)\%\s*$/)) {
|
||||
var progress = Math.max( 0, Math.min( 100, parseInt( RegExp.$1 ) ) ) / 100;
|
||||
stream.write({
|
||||
progress: progress
|
||||
});
|
||||
}
|
||||
else {
|
||||
// otherwise just log it
|
||||
if (job.params.annotate) {
|
||||
var dargs = Tools.getDateArgs( new Date() );
|
||||
line = '[' + dargs.yyyy_mm_dd + ' ' + dargs.hh_mi_ss + '] ' + line;
|
||||
}
|
||||
fs.appendFileSync(job.log_file, line);
|
||||
}
|
||||
} );
|
||||
|
||||
cstream.on('error', function(err, text) {
|
||||
// Probably a JSON parse error (child emitting garbage)
|
||||
if (text) fs.appendFileSync(job.log_file, text + "\n");
|
||||
} );
|
||||
|
||||
child.on('error', function (err) {
|
||||
// child error
|
||||
stream.write({
|
||||
complete: 1,
|
||||
code: 1,
|
||||
description: "Script failed: " + Tools.getErrorDescription(err)
|
||||
});
|
||||
|
||||
fs.unlink( script_file, function(err) {;} );
|
||||
} );
|
||||
|
||||
child.on('exit', function (code, signal) {
|
||||
// child exited
|
||||
if (kill_timer) clearTimeout(kill_timer);
|
||||
code = (code || signal || 0);
|
||||
|
||||
var data = {
|
||||
complete: 1,
|
||||
code: code,
|
||||
description: code ? ("Script exited with code: " + code) : ""
|
||||
};
|
||||
|
||||
if (stderr_buffer.length && stderr_buffer.match(/\S/)) {
|
||||
data.html = {
|
||||
title: "Error Output",
|
||||
content: "<pre>" + stderr_buffer.replace(/</g, '<').trim() + "</pre>"
|
||||
};
|
||||
|
||||
if (code) {
|
||||
// possibly augment description with first line of stderr, if not too insane
|
||||
var stderr_line = stderr_buffer.trim().split(/\n/).shift();
|
||||
if (stderr_line.length < 256) data.description += ": " + stderr_line;
|
||||
}
|
||||
}
|
||||
|
||||
stream.write(data);
|
||||
fs.unlink( script_file, function(err) {;} );
|
||||
} ); // exit
|
||||
|
||||
// silence EPIPE errors on child STDIN
|
||||
child.stdin.on('error', function(err) {
|
||||
// ignore
|
||||
} );
|
||||
|
||||
// track stderr separately for display purposes
|
||||
child.stderr.setEncoding('utf8');
|
||||
child.stderr.on('data', function(data) {
|
||||
// keep first 32K in RAM, but log everything
|
||||
if (stderr_buffer.length < 32768) stderr_buffer += data;
|
||||
else if (!stderr_buffer.match(/\.\.\.$/)) stderr_buffer += '...';
|
||||
|
||||
fs.appendFileSync(job.log_file, data);
|
||||
});
|
||||
|
||||
// pass job down to child process (harmless for shell, useful for php/perl/node)
|
||||
cstream.write( job );
|
||||
child.stdin.end();
|
||||
|
||||
// Handle shutdown
|
||||
process.on('SIGTERM', function() {
|
||||
console.log("Caught SIGTERM, killing child: " + child.pid);
|
||||
|
||||
kill_timer = setTimeout( function() {
|
||||
// child didn't die, kill with prejudice
|
||||
console.log("Child did not exit, killing harder: " + child.pid);
|
||||
child.kill('SIGKILL');
|
||||
}, 9 * 1000 );
|
||||
|
||||
// try killing nicely first
|
||||
child.kill('SIGTERM');
|
||||
} );
|
||||
|
||||
} ); // stream
|
610
workspaces/base-workspace/Cronicle-0.8.61/bin/storage-cli.js
Executable file
|
@ -0,0 +1,610 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// CLI for Storage System
|
||||
// Copyright (c) 2015 Joseph Huckaby
|
||||
// Released under the MIT License
|
||||
|
||||
var path = require('path');
|
||||
var cp = require('child_process');
|
||||
var os = require('os');
|
||||
var fs = require('fs');
|
||||
var async = require('async');
|
||||
var bcrypt = require('bcrypt-node');
|
||||
|
||||
var Args = require('pixl-args');
|
||||
var Tools = require('pixl-tools');
|
||||
var StandaloneStorage = require('pixl-server-storage/standalone');
|
||||
|
||||
// chdir to the proper server root dir
|
||||
process.chdir( path.dirname( __dirname ) );
|
||||
|
||||
// load app's config file
|
||||
var config = require('../conf/config.json');
|
||||
|
||||
// shift commands off beginning of arg array
|
||||
var argv = JSON.parse( JSON.stringify(process.argv.slice(2)) );
|
||||
var commands = [];
|
||||
while (argv.length && !argv[0].match(/^\-/)) {
|
||||
commands.push( argv.shift() );
|
||||
}
|
||||
|
||||
// now parse rest of cmdline args, if any
|
||||
var args = new Args( argv, {
|
||||
debug: false,
|
||||
verbose: false,
|
||||
quiet: false
|
||||
} );
|
||||
args = args.get(); // simple hash
|
||||
|
||||
// copy debug flag into config (for standalone)
|
||||
config.Storage.debug = args.debug;
|
||||
|
||||
var print = function(msg) {
|
||||
// print message to console
|
||||
if (!args.quiet) process.stdout.write(msg);
|
||||
};
|
||||
var verbose = function(msg) {
|
||||
// print only in verbose mode
|
||||
if (args.verbose) print(msg);
|
||||
};
|
||||
var warn = function(msg) {
|
||||
// print to stderr unless quiet
|
||||
if (!args.quiet) process.stderr.write(msg);
|
||||
};
|
||||
var verbose_warn = function(msg) {
|
||||
// verbose print to stderr unless quiet
|
||||
if (args.verbose && !args.quiet) process.stderr.write(msg);
|
||||
};
|
||||
|
||||
if (config.uid && (process.getuid() != 0)) {
|
||||
print( "ERROR: Must be root to use this script.\n" );
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// determine server hostname
|
||||
var hostname = (process.env['HOSTNAME'] || process.env['HOST'] || os.hostname()).toLowerCase();
|
||||
|
||||
// find the first external IPv4 address
|
||||
var ip = '';
|
||||
var ifaces = os.networkInterfaces();
|
||||
var addrs = [];
|
||||
for (var key in ifaces) {
|
||||
if (ifaces[key] && ifaces[key].length) {
|
||||
Array.from(ifaces[key]).forEach( function(item) { addrs.push(item); } );
|
||||
}
|
||||
}
|
||||
var addr = Tools.findObject( addrs, { family: 'IPv4', internal: false } );
|
||||
if (addr && addr.address && addr.address.match(/^\d+\.\d+\.\d+\.\d+$/)) {
|
||||
ip = addr.address;
|
||||
}
|
||||
else {
|
||||
print( "ERROR: Could not determine server's IP address.\n" );
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// util.isArray is DEPRECATED??? Nooooooooode!
|
||||
var isArray = Array.isArray || util.isArray;
|
||||
|
||||
// prevent logging transactions to STDOUT
|
||||
config.Storage.log_event_types = {};
|
||||
|
||||
// allow APPNAME_key env vars to override config
|
||||
var env_regex = new RegExp( "^CRONICLE_(.+)$" );
|
||||
for (var env_key in process.env) {
|
||||
if (env_key.match(env_regex)) {
|
||||
var env_path = RegExp.$1.trim().replace(/^_+/, '').replace(/_+$/, '').replace(/__/g, '/');
|
||||
var env_value = process.env[env_key].toString();
|
||||
|
||||
// massage value into various types
|
||||
if (env_value === 'true') env_value = true;
|
||||
else if (env_value === 'false') env_value = false;
|
||||
else if (env_value.match(/^\-?\d+$/)) env_value = parseInt(env_value);
|
||||
else if (env_value.match(/^\-?\d+\.\d+$/)) env_value = parseFloat(env_value);
|
||||
|
||||
Tools.setPath(config, env_path, env_value);
|
||||
}
|
||||
}
|
||||
|
||||
// construct standalone storage server
|
||||
var storage = new StandaloneStorage(config.Storage, function(err) {
|
||||
if (err) throw err;
|
||||
// storage system is ready to go
|
||||
|
||||
// become correct user
|
||||
if (config.uid && (process.getuid() == 0)) {
|
||||
verbose( "Switching to user: " + config.uid + "\n" );
|
||||
process.setuid( config.uid );
|
||||
}
|
||||
|
||||
// custom job data expire handler
|
||||
storage.addRecordType( 'cronicle_job', {
|
||||
'delete': function(key, value, callback) {
|
||||
storage.delete( key, function(err) {
|
||||
storage.delete( key + '/log.txt.gz', function(err) {
|
||||
callback();
|
||||
} ); // delete
|
||||
} ); // delete
|
||||
}
|
||||
} );
|
||||
|
||||
// process command
|
||||
var cmd = commands.shift();
|
||||
verbose("\n");
|
||||
|
||||
switch (cmd) {
|
||||
case 'setup':
|
||||
case 'install':
|
||||
// setup new master server
|
||||
var setup = require('../conf/setup.json');
|
||||
|
||||
// make sure this is only run once
|
||||
storage.get( 'global/users', function(err) {
|
||||
if (!err) {
|
||||
print( "Storage has already been set up. There is no need to run this command again.\n\n" );
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async.eachSeries( setup.storage,
|
||||
function(params, callback) {
|
||||
verbose( "Executing: " + JSON.stringify(params) + "\n" );
|
||||
// [ "listCreate", "global/users", { "page_size": 100 } ]
|
||||
var func = params.shift();
|
||||
params.push( callback );
|
||||
|
||||
// massage a few params
|
||||
if (typeof(params[1]) == 'object') {
|
||||
var obj = params[1];
|
||||
if (obj.created) obj.created = Tools.timeNow(true);
|
||||
if (obj.modified) obj.modified = Tools.timeNow(true);
|
||||
if (obj.regexp && (obj.regexp == '_HOSTNAME_')) obj.regexp = '^(' + Tools.escapeRegExp( hostname ) + ')$';
|
||||
if (obj.hostname && (obj.hostname == '_HOSTNAME_')) obj.hostname = hostname;
|
||||
if (obj.ip && (obj.ip == '_IP_')) obj.ip = ip;
|
||||
}
|
||||
|
||||
// call storage directly
|
||||
storage[func].apply( storage, params );
|
||||
},
|
||||
function(err) {
|
||||
if (err) throw err;
|
||||
|
||||
print("\n");
|
||||
print( "Setup completed successfully!\n" );
|
||||
print( "This server ("+hostname+") has been added as the single primary master server.\n" );
|
||||
print( "An administrator account has been created with username 'admin' and password 'admin'.\n" );
|
||||
print( "You should now be able to start the service by typing: '/opt/cronicle/bin/control.sh start'\n" );
|
||||
print( "Then, the web interface should be available at: http://"+hostname+":"+config.WebServer.http_port+"/\n" );
|
||||
print( "Please allow for up to 60 seconds for the server to become master.\n\n" );
|
||||
|
||||
storage.shutdown( function() { process.exit(0); } );
|
||||
}
|
||||
);
|
||||
} );
|
||||
break;
|
||||
|
||||
case 'admin':
|
||||
// create or replace admin account
|
||||
// Usage: ./storage-cli.js admin USERNAME PASSWORD [EMAIL]
|
||||
var username = commands.shift();
|
||||
var password = commands.shift();
|
||||
var email = commands.shift() || 'admin@localhost';
|
||||
if (!username || !password) {
|
||||
print( "\nUsage: bin/storage-cli.js admin USERNAME PASSWORD [EMAIL]\n\n" );
|
||||
process.exit(1);
|
||||
}
|
||||
if (!username.match(/^[\w\-\.]+$/)) {
|
||||
print( "\nERROR: Username must contain only alphanumerics, dash and period.\n\n" );
|
||||
process.exit(1);
|
||||
}
|
||||
username = username.toLowerCase();
|
||||
|
||||
var user = {
|
||||
username: username,
|
||||
password: password,
|
||||
full_name: "Administrator",
|
||||
email: email
|
||||
};
|
||||
|
||||
user.active = 1;
|
||||
user.created = user.modified = Tools.timeNow(true);
|
||||
user.salt = Tools.generateUniqueID( 64, user.username );
|
||||
user.password = bcrypt.hashSync( user.password + user.salt );
|
||||
user.privileges = { admin: 1 };
|
||||
|
||||
storage.put( 'users/' + username, user, function(err) {
|
||||
if (err) throw err;
|
||||
print( "\nAdministrator '"+username+"' created successfully.\n" );
|
||||
print("\n");
|
||||
|
||||
storage.shutdown( function() { process.exit(0); } );
|
||||
} );
|
||||
break;
|
||||
|
||||
case 'get':
|
||||
case 'fetch':
|
||||
case 'view':
|
||||
case 'cat':
|
||||
// get storage key
|
||||
// Usage: ./storage-cli.js get users/jhuckaby
|
||||
var key = commands.shift();
|
||||
storage.get( key, function(err, data) {
|
||||
if (err) throw err;
|
||||
if (storage.isBinaryKey(key)) print( data.toString() + "\n" );
|
||||
else print( ((typeof(data) == 'object') ? JSON.stringify(data, null, "\t") : data) + "\n" );
|
||||
print("\n");
|
||||
|
||||
storage.shutdown( function() { process.exit(0); } );
|
||||
} );
|
||||
break;
|
||||
|
||||
case 'put':
|
||||
case 'save':
|
||||
case 'store':
|
||||
// put storage key (read data from STDIN)
|
||||
// Usage: cat USER.json | ./storage-cli.js put users/jhuckaby
|
||||
var key = commands.shift();
|
||||
var json_raw = '';
|
||||
var rl = require('readline').createInterface({ input: process.stdin });
|
||||
rl.on('line', function(line) { json_raw += line; });
|
||||
rl.on('close', function() {
|
||||
print( "Writing record from STDIN: " + key + "\n" );
|
||||
|
||||
var data = null;
|
||||
try { data = JSON.parse(json_raw); }
|
||||
catch (err) {
|
||||
warn( "Failed to parse JSON for key: " + key + ": " + err + "\n" );
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
storage.put( key, data, function(err) {
|
||||
if (err) {
|
||||
warn( "Failed to store record: " + key + ": " + err + "\n" );
|
||||
process.exit(1);
|
||||
}
|
||||
print("Record successfully saved: "+key+"\n");
|
||||
|
||||
storage.shutdown( function() { process.exit(0); } );
|
||||
} );
|
||||
});
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
case 'vi':
|
||||
var key = commands.shift();
|
||||
|
||||
if ((cmd == 'edit') && !process.env.EDITOR) {
|
||||
warn( "No EDITOR environment variable is set.\n" );
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
storage.get( key, function(err, data) {
|
||||
if (err) data = {};
|
||||
print("Spawning editor to edit record: " + key + "\n");
|
||||
|
||||
// save to local temp file
|
||||
var temp_file = path.join( os.tmpdir(), 'cli-temp-' + process.pid + '.json' );
|
||||
fs.writeFileSync( temp_file, JSON.stringify(data, null, "\t") + "\n" );
|
||||
var stats = fs.statSync( temp_file );
|
||||
var old_mod = Math.floor( stats.mtime.getTime() / 1000 );
|
||||
|
||||
// spawn vi but inherit terminal
|
||||
var child = cp.spawn( (cmd == 'vi') ? 'vi' : process.env.EDITOR, [temp_file], {
|
||||
stdio: 'inherit'
|
||||
} );
|
||||
child.on('exit', function (e, code) {
|
||||
var stats = fs.statSync( temp_file );
|
||||
var new_mod = Math.floor( stats.mtime.getTime() / 1000 );
|
||||
if (new_mod != old_mod) {
|
||||
print("Saving new data back into record: "+key+"\n");
|
||||
|
||||
var json_raw = fs.readFileSync( temp_file, { encoding: 'utf8' } );
|
||||
fs.unlinkSync( temp_file );
|
||||
|
||||
var data = JSON.parse( json_raw );
|
||||
|
||||
storage.put( key, data, function(err, data) {
|
||||
if (err) throw err;
|
||||
print("Record successfully saved with your changes: "+key+"\n");
|
||||
|
||||
storage.shutdown( function() { process.exit(0); } );
|
||||
} );
|
||||
}
|
||||
else {
|
||||
fs.unlinkSync( temp_file );
|
||||
print("File has not been changed, record was not touched: "+key+"\n");
|
||||
|
||||
storage.shutdown( function() { process.exit(0); } );
|
||||
}
|
||||
} );
|
||||
|
||||
} ); // got data
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
// delete storage key
|
||||
// Usage: ./storage-cli.js delete users/jhuckaby
|
||||
var key = commands.shift();
|
||||
storage.delete( key, function(err, data) {
|
||||
if (err) throw err;
|
||||
print("Record '"+key+"' deleted successfully.\n");
|
||||
print("\n");
|
||||
|
||||
storage.shutdown( function() { process.exit(0); } );
|
||||
} );
|
||||
break;
|
||||
|
||||
case 'list_create':
|
||||
// create new list
|
||||
// Usage: ./storage-cli.js list_create key
|
||||
var key = commands.shift();
|
||||
storage.listCreate( key, null, function(err) {
|
||||
if (err) throw err;
|
||||
print("List created successfully: " + key + "\n");
|
||||
print("\n");
|
||||
|
||||
storage.shutdown( function() { process.exit(0); } );
|
||||
} );
|
||||
break;
|
||||
|
||||
case 'list_pop':
|
||||
// pop item off end of list
|
||||
// Usage: ./storage-cli.js list_pop key
|
||||
var key = commands.shift();
|
||||
storage.listPop( key, function(err, item) {
|
||||
if (err) throw err;
|
||||
print("Item popped off list: " + key + ": " + JSON.stringify(item, null, "\t") + "\n");
|
||||
print("\n");
|
||||
|
||||
storage.shutdown( function() { process.exit(0); } );
|
||||
} );
|
||||
break;
|
||||
|
||||
case 'list_get':
|
||||
// fetch items from list
|
||||
// Usage: ./storage-cli.js list_get key idx len
|
||||
var key = commands.shift();
|
||||
var idx = parseInt( commands.shift() || 0 );
|
||||
var len = parseInt( commands.shift() || 0 );
|
||||
storage.listGet( key, idx, len, function(err, items) {
|
||||
if (err) throw err;
|
||||
print("Got " + items.length + " items.\n");
|
||||
print("Items from list: " + key + ": " + JSON.stringify(items, null, "\t") + "\n");
|
||||
print("\n");
|
||||
|
||||
storage.shutdown( function() { process.exit(0); } );
|
||||
} );
|
||||
break;
|
||||
|
||||
case 'list_info':
|
||||
// fetch info about list
|
||||
// Usage: ./storage-cli.js list_info key
|
||||
var key = commands.shift();
|
||||
|
||||
storage.listGetInfo( key, function(err, list) {
|
||||
if (err) throw err;
|
||||
print("List Header: " + key + ": " + JSON.stringify(list, null, "\t") + "\n\n");
|
||||
var page_idx = list.first_page;
|
||||
var item_idx = 0;
|
||||
async.whilst(
|
||||
function() { return page_idx <= list.last_page; },
|
||||
function(callback) {
|
||||
// load each page
|
||||
storage._listLoadPage(key, page_idx++, false, function(err, page) {
|
||||
if (err) return callback(err);
|
||||
print("Page " + Math.floor(page_idx - 1) + ": " + page.items.length + " items\n");
|
||||
callback();
|
||||
} ); // page loaded
|
||||
},
|
||||
function(err) {
|
||||
// all pages iterated
|
||||
if (err) throw err;
|
||||
print("\n");
|
||||
|
||||
storage.shutdown( function() { process.exit(0); } );
|
||||
} // pages complete
|
||||
); // whilst
|
||||
} );
|
||||
break;
|
||||
|
||||
case 'list_delete':
|
||||
// delete list
|
||||
// Usage: ./storage-cli.js list_delete key
|
||||
var key = commands.shift();
|
||||
storage.listDelete( key, null, function(err) {
|
||||
if (err) throw err;
|
||||
print("List deleted successfully: " + key + "\n");
|
||||
print("\n");
|
||||
|
||||
storage.shutdown( function() { process.exit(0); } );
|
||||
} );
|
||||
break;
|
||||
|
||||
case 'maint':
|
||||
case 'maintenance':
|
||||
// perform daily maintenance, specify date or defaults to current day
|
||||
// Usage: ./storage-cli.js maint 2015-05-31
|
||||
storage.runMaintenance( commands.shift(), function() {
|
||||
print( "Daily maintenance completed successfully.\n" );
|
||||
print("\n");
|
||||
|
||||
storage.shutdown( function() { process.exit(0); } );
|
||||
} );
|
||||
break;
|
||||
|
||||
case 'export':
|
||||
// export all storage data (except completed jobs, sessions)
|
||||
var file = commands.shift();
|
||||
export_data(file);
|
||||
break;
|
||||
|
||||
case 'import':
|
||||
// import storage data from file
|
||||
var file = commands.shift();
|
||||
import_data(file);
|
||||
break;
|
||||
|
||||
default:
|
||||
print("Unknown command: " + cmd + "\n");
|
||||
storage.shutdown( function() { process.exit(0); } );
|
||||
break;
|
||||
|
||||
} // switch
|
||||
});
|
||||
|
||||
function export_data(file) {
|
||||
// export data to file or stdout (except for completed jobs, logs, and sessions)
|
||||
// one record per line: KEY - JSON
|
||||
var stream = file ? fs.createWriteStream(file) : process.stdout;
|
||||
|
||||
// file header (for humans)
|
||||
var file_header = "# Cronicle Data Export v1.0\n" +
|
||||
"# Hostname: " + hostname + "\n" +
|
||||
"# Date/Time: " + (new Date()).toString() + "\n" +
|
||||
"# Format: KEY - JSON\n\n";
|
||||
|
||||
stream.write( file_header );
|
||||
verbose_warn( file_header );
|
||||
|
||||
if (file) verbose_warn("Exporting to file: " + file + "\n\n");
|
||||
|
||||
// need to handle users separately, as they're stored as a list + individual records
|
||||
storage.listEach( 'global/users',
|
||||
function(item, idx, callback) {
|
||||
var username = item.username;
|
||||
var key = 'users/' + username.toString().toLowerCase().replace(/\W+/g, '');
|
||||
verbose_warn( "Exporting user: " + username + "\n" );
|
||||
|
||||
storage.get( key, function(err, user) {
|
||||
if (err) {
|
||||
// user deleted?
|
||||
warn( "\nFailed to fetch user: " + key + ": " + err + "\n\n" );
|
||||
return callback();
|
||||
}
|
||||
|
||||
stream.write( key + ' - ' + JSON.stringify(user) + "\n", 'utf8', callback );
|
||||
} ); // get
|
||||
},
|
||||
function(err) {
|
||||
// ignoring errors here
|
||||
// proceed to the rest of the lists
|
||||
async.eachSeries(
|
||||
[
|
||||
'global/users',
|
||||
'global/plugins',
|
||||
'global/categories',
|
||||
'global/server_groups',
|
||||
'global/schedule',
|
||||
'global/servers',
|
||||
'global/api_keys'
|
||||
],
|
||||
function(list_key, callback) {
|
||||
// first get the list header
|
||||
verbose_warn( "Exporting list: " + list_key + "\n" );
|
||||
|
||||
storage.get( list_key, function(err, list) {
|
||||
if (err) return callback( new Error("Failed to fetch list: " + list_key + ": " + err) );
|
||||
|
||||
stream.write( list_key + ' - ' + JSON.stringify(list) + "\n" );
|
||||
|
||||
// now iterate over all the list pages
|
||||
var page_idx = list.first_page;
|
||||
|
||||
async.whilst(
|
||||
function() { return page_idx <= list.last_page; },
|
||||
function(callback) {
|
||||
// load each page
|
||||
var page_key = list_key + '/' + page_idx;
|
||||
page_idx++;
|
||||
|
||||
verbose_warn( "Exporting list page: " + page_key + "\n");
|
||||
|
||||
storage.get(page_key, function(err, page) {
|
||||
if (err) return callback( new Error("Failed to fetch list page: " + page_key + ": " + err) );
|
||||
|
||||
// write page data
|
||||
stream.write( page_key + ' - ' + JSON.stringify(page) + "\n", 'utf8', callback );
|
||||
} ); // page get
|
||||
}, // iterator
|
||||
callback
|
||||
); // whilst
|
||||
|
||||
} ); // get
|
||||
}, // iterator
|
||||
function(err) {
|
||||
if (err) {
|
||||
warn( "\nEXPORT ERROR: " + err + "\n" );
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
verbose_warn( "\nExport completed at " + (new Date()).toString() + ".\nExiting.\n\n" );
|
||||
|
||||
if (file) stream.end();
|
||||
|
||||
storage.shutdown( function() { process.exit(0); } );
|
||||
} // done done
|
||||
); // list eachSeries
|
||||
} // done with users
|
||||
); // users listEach
|
||||
};
|
||||
|
||||
function import_data(file) {
|
||||
// import storage data from specified file or stdin
|
||||
// one record per line: KEY - JSON
|
||||
print( "\nCronicle Data Importer v1.0\n" );
|
||||
if (file) print( "Importing from file: " + file + "\n" );
|
||||
else print( "Importing from STDIN\n" );
|
||||
print( "\n" );
|
||||
|
||||
var count = 0;
|
||||
var queue = async.queue( function(line, callback) {
|
||||
// process each line
|
||||
if (line.match(/^(\w[\w\-\.\/]*)\s+\-\s+(\{.+\})\s*$/)) {
|
||||
var key = RegExp.$1;
|
||||
var json_raw = RegExp.$2;
|
||||
print( "Importing record: " + key + "\n" );
|
||||
|
||||
var data = null;
|
||||
try { data = JSON.parse(json_raw); }
|
||||
catch (err) {
|
||||
warn( "Failed to parse JSON for key: " + key + ": " + err + "\n" );
|
||||
return callback();
|
||||
}
|
||||
|
||||
storage.put( key, data, function(err) {
|
||||
if (err) {
|
||||
warn( "Failed to store record: " + key + ": " + err + "\n" );
|
||||
return callback();
|
||||
}
|
||||
count++;
|
||||
callback();
|
||||
} );
|
||||
}
|
||||
else callback();
|
||||
}, 1 );
|
||||
|
||||
// setup readline to line-read from file or stdin
|
||||
var readline = require('readline');
|
||||
var rl = readline.createInterface({
|
||||
input: file ? fs.createReadStream(file) : process.stdin
|
||||
});
|
||||
|
||||
rl.on('line', function(line) {
|
||||
// enqueue each line
|
||||
queue.push( line );
|
||||
});
|
||||
|
||||
rl.on('close', function() {
|
||||
// end of input stream
|
||||
var complete = function() {
|
||||
// finally, delete state so cronicle recreates it
|
||||
storage.delete( 'global/state', function(err) {
|
||||
// ignore error here, as state may not exist yet
|
||||
print( "\nImport complete. " + count + " records imported.\nExiting.\n\n" );
|
||||
storage.shutdown( function() { process.exit(0); } );
|
||||
});
|
||||
};
|
||||
|
||||
// fire complete on queue drain
|
||||
if (queue.idle()) complete();
|
||||
else queue.drain = complete;
|
||||
}); // rl close
|
||||
};
|
440
workspaces/base-workspace/Cronicle-0.8.61/bin/storage-migrate.js
Executable file
|
@ -0,0 +1,440 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Cronicle Storage Migration System
|
||||
// Copyright (c) 2018 Joseph Huckaby
|
||||
// Released under the MIT License
|
||||
|
||||
// Instructions:
|
||||
// Edit your Cronicle conf/config.json file, and make a copy of the `Storage` element.
|
||||
// Name the copy `NewStorage`, and put all the new settings in there, that you are migrating to.
|
||||
|
||||
// Command-Line Usage:
|
||||
// bin/storage-migrate.js
|
||||
// --debug: Echo debug log to console
|
||||
// --verbose: List every key as it is copied
|
||||
// --dryrun: Do not write any changes
|
||||
|
||||
// After completion, delete `Storage`, and rename `NewStorage` to `Storage`, and you're migrated.
|
||||
|
||||
var Path = require('path');
|
||||
var os = require('os');
|
||||
var fs = require('fs');
|
||||
var async = require('async');
|
||||
var Logger = require('pixl-logger');
|
||||
var cli = require('pixl-cli');
|
||||
var args = cli.args;
|
||||
cli.global();
|
||||
|
||||
var StandaloneStorage = require('pixl-server-storage/standalone');
|
||||
|
||||
// chdir to the proper server root dir
|
||||
process.chdir( Path.dirname( __dirname ) );
|
||||
|
||||
// load app's config file
|
||||
var config = require('../conf/config.json');
|
||||
|
||||
var StorageMigrator = {
|
||||
|
||||
version: "1.0.0",
|
||||
|
||||
run: function() {
|
||||
// here we go
|
||||
var self = this;
|
||||
|
||||
// setup logger
|
||||
var log_file = Path.join( config.log_dir, 'StorageMigration.log' );
|
||||
this.logger = new Logger( log_file, config.log_columns, {
|
||||
debugLevel: config.debug_level,
|
||||
sync: true,
|
||||
echo: args.debug,
|
||||
color: args.color
|
||||
} );
|
||||
|
||||
print("\n");
|
||||
this.logPrint(1, "Cronicle Storage Migration Script v" + this.version + " starting up");
|
||||
this.logPrint(2, "Starting storage engines");
|
||||
|
||||
if (!config.Storage) this.fatal("Your Cronicle configuration lacks a 'Storage' property");
|
||||
if (!config.NewStorage) this.fatal("Your Cronicle configuration lacks a 'NewStorage' property.");
|
||||
|
||||
if (config.uid && (process.getuid() != 0)) {
|
||||
this.fatal( "Must be root to use the storage migration script." );
|
||||
}
|
||||
|
||||
// check pid file
|
||||
if (config.pid_file) try {
|
||||
var pid = fs.readFileSync( config.pid_file, 'utf8' );
|
||||
if (pid && process.kill(pid, 0)) this.fatal("Please shut down Cronicle before migrating storage.");
|
||||
}
|
||||
catch (e) {;}
|
||||
|
||||
// massage config, override logger
|
||||
config.Storage.logger = self.logger;
|
||||
config.Storage.log_event_types = { all: 1 };
|
||||
|
||||
config.NewStorage.logger = self.logger;
|
||||
config.NewStorage.log_event_types = { all: 1 };
|
||||
|
||||
// start both standalone storage instances
|
||||
async.series(
|
||||
[
|
||||
function(callback) {
|
||||
self.oldStorage = new StandaloneStorage(config.Storage, callback);
|
||||
},
|
||||
function(callback) {
|
||||
self.newStorage = new StandaloneStorage(config.NewStorage, callback);
|
||||
},
|
||||
],
|
||||
function(err) {
|
||||
if (err) self.fatal("Failed to start storage engine: " + err);
|
||||
|
||||
self.logPrint(2, "Storage engines are ready to go");
|
||||
|
||||
// become correct user
|
||||
if (config.uid && (process.getuid() == 0)) {
|
||||
self.logPrint( 3, "Switching to user: " + config.uid );
|
||||
process.setuid( config.uid );
|
||||
}
|
||||
|
||||
self.testStorage();
|
||||
}
|
||||
); // series
|
||||
},
|
||||
|
||||
testStorage: function() {
|
||||
// test both old and new storage
|
||||
var self = this;
|
||||
this.logDebug(3, "Testing storage engines");
|
||||
|
||||
async.series(
|
||||
[
|
||||
function(callback) {
|
||||
self.oldStorage.get('global/users', callback);
|
||||
},
|
||||
function(callback) {
|
||||
self.newStorage.put('test/test1', { "foo1": "bar1" }, function(err) {
|
||||
if (err) return callback(err);
|
||||
|
||||
self.newStorage.delete('test/test1', function(err) {
|
||||
if (err) return callback(err);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
},
|
||||
],
|
||||
function(err) {
|
||||
if (err) self.fatal("Storage test failure: " + err);
|
||||
|
||||
self.logPrint(2, "Storage engines tested successfully");
|
||||
self.startMigration();
|
||||
}
|
||||
); // series
|
||||
},
|
||||
|
||||
startMigration: function() {
|
||||
// start migration process
|
||||
var self = this;
|
||||
this.logPrint(3, "Starting migration");
|
||||
|
||||
this.timeStart = Tools.timeNow(true);
|
||||
this.numRecords = 0;
|
||||
|
||||
this.copyKey( 'global/state', { ignore: true } );
|
||||
|
||||
var lists = [
|
||||
'global/users',
|
||||
'global/plugins',
|
||||
'global/categories',
|
||||
'global/server_groups',
|
||||
'global/schedule',
|
||||
'global/servers',
|
||||
'global/api_keys'
|
||||
];
|
||||
lists.forEach( function(key) { self.copyList(key); } );
|
||||
|
||||
// these lists technically may not exist yet:
|
||||
this.copyList( 'logs/completed', { ignore: true } );
|
||||
this.copyList( 'logs/activity', { ignore: true } );
|
||||
|
||||
this.migrateUsers();
|
||||
},
|
||||
|
||||
migrateUsers: function() {
|
||||
var self = this;
|
||||
this.logPrint(3, "Migrating user records");
|
||||
|
||||
this.oldStorage.listEach( 'global/users',
|
||||
function(user, idx, callback) {
|
||||
var username = self.normalizeUsername(user.username);
|
||||
var key = 'users/' + username;
|
||||
self.copyKey( key );
|
||||
process.nextTick( callback );
|
||||
},
|
||||
function() {
|
||||
self.migrateCompletedEvents();
|
||||
}
|
||||
); // listEach
|
||||
},
|
||||
|
||||
migrateCompletedEvents: function() {
|
||||
var self = this;
|
||||
this.logPrint(3, "Migrating completed events");
|
||||
|
||||
this.oldStorage.listEach( 'global/schedule',
|
||||
function(event, idx, callback) {
|
||||
var key = 'logs/events/' + event.id;
|
||||
self.copyList( key, { ignore: true } );
|
||||
process.nextTick( callback );
|
||||
},
|
||||
function() {
|
||||
self.migrateCompletedJobs();
|
||||
}
|
||||
); // listEach
|
||||
},
|
||||
|
||||
migrateCompletedJobs: function() {
|
||||
var self = this;
|
||||
this.logPrint(3, "Migrating completed jobs");
|
||||
|
||||
var unique_cleanup_lists = {};
|
||||
|
||||
this.oldStorage.listEach( 'logs/completed',
|
||||
function(job, idx, callback) {
|
||||
self.copyKey( 'jobs/' + job.id, { ignore: true } );
|
||||
self.copyKey( 'jobs/' + job.id + '/log.txt.gz', { ignore: true } );
|
||||
|
||||
var time_end = job.time_start + job.elapsed;
|
||||
var key_expires = time_end + (86400 * config.job_data_expire_days);
|
||||
var log_expires = time_end + (86400 * (job.log_expire_days || config.job_data_expire_days));
|
||||
|
||||
// get hash of unique exp dates, to grab cleanup lists
|
||||
var dargs = Tools.getDateArgs( key_expires );
|
||||
var cleanup_list_path = '_cleanup/' + dargs.yyyy + '/' + dargs.mm + '/' + dargs.dd;
|
||||
unique_cleanup_lists[ cleanup_list_path ] = true;
|
||||
|
||||
dargs = Tools.getDateArgs( log_expires );
|
||||
cleanup_list_path = '_cleanup/' + dargs.yyyy + '/' + dargs.mm + '/' + dargs.dd;
|
||||
unique_cleanup_lists[ cleanup_list_path ] = true;
|
||||
|
||||
process.nextTick( callback );
|
||||
},
|
||||
function() {
|
||||
// now queue up list copies for cleanup lists
|
||||
self.logPrint(3, "Migrating cleanup lists");
|
||||
|
||||
for (var key in unique_cleanup_lists) {
|
||||
self.copyList( key, { ignore: true } );
|
||||
}
|
||||
|
||||
// Note: we are deliberately skipping the cleanup master hash, e.g. _cleanup/expires
|
||||
// This is only needed for records that CHANGE their expiration after the fact,
|
||||
// which never happens with Cronicle.
|
||||
|
||||
self.waitForQueue();
|
||||
}
|
||||
); // listEach
|
||||
},
|
||||
|
||||
waitForQueue: function() {
|
||||
// wait for storage to complete queue
|
||||
var self = this;
|
||||
this.logPrint(3, "Waiting for storage queue");
|
||||
|
||||
this.queueMax = this.newStorage.queue.length();
|
||||
this.logDebug(5, "Queue length: " + this.queueMax);
|
||||
|
||||
if (!args.verbose && !args.debug && !args.dryrun && !args.dots) {
|
||||
cli.progress.start({ max: this.queueMax });
|
||||
}
|
||||
|
||||
this.newStorage.waitForQueueDrain( this.finish.bind(this) );
|
||||
},
|
||||
|
||||
finish: function() {
|
||||
// all done
|
||||
var self = this;
|
||||
var elapsed = Tools.timeNow(true) - this.timeStart;
|
||||
|
||||
cli.progress.end();
|
||||
|
||||
print("\n");
|
||||
this.logPrint(1, "Storage migration complete!");
|
||||
this.logPrint(2, Tools.commify(this.numRecords) + " total records copied in " + Tools.getTextFromSeconds(elapsed, false, true) + ".");
|
||||
this.logPrint(4, "You should now overwrite 'Storage' with 'NewStorage' in your config.json.");
|
||||
this.logPrint(3, "Shutting down");
|
||||
|
||||
async.series(
|
||||
[
|
||||
function(callback) {
|
||||
self.oldStorage.shutdown(callback);
|
||||
},
|
||||
function(callback) {
|
||||
self.newStorage.shutdown(callback);
|
||||
},
|
||||
],
|
||||
function(err) {
|
||||
self.logPrint(3, "Shutdown complete, exiting.");
|
||||
print("\n");
|
||||
process.exit(0);
|
||||
}
|
||||
); // series
|
||||
},
|
||||
|
||||
normalizeUsername: function(username) {
|
||||
// lower-case, strip all non-alpha
|
||||
if (!username) return '';
|
||||
return username.toString().toLowerCase().replace(/\W+/g, '');
|
||||
},
|
||||
|
||||
copyKey: function(key, opts) {
|
||||
// enqueue key for copy
|
||||
this.logDebug(9, "Enqueuing key for copy: " + key, opts);
|
||||
|
||||
this.newStorage.enqueue( Tools.mergeHashes({
|
||||
action: 'custom',
|
||||
copy_type: 'key',
|
||||
copy_key: key,
|
||||
handler: this.dequeue.bind(this)
|
||||
}, opts || {} ));
|
||||
},
|
||||
|
||||
copyList: function(key, opts) {
|
||||
// enqueue list for copy
|
||||
this.logDebug(9, "Enqueuing list for copy: " + key, opts);
|
||||
|
||||
this.newStorage.enqueue( Tools.mergeHashes({
|
||||
action: 'custom',
|
||||
copy_type: 'list',
|
||||
copy_key: key,
|
||||
handler: this.dequeue.bind(this)
|
||||
}, opts || {} ));
|
||||
},
|
||||
|
||||
dequeue: function(task, callback) {
|
||||
// copy list or key
|
||||
var self = this;
|
||||
var key = task.copy_key;
|
||||
|
||||
switch (task.copy_type) {
|
||||
case 'list':
|
||||
// copy whole list
|
||||
this.oldStorage.get(key, function(err, list) {
|
||||
if (err) {
|
||||
if (task.ignore) return callback();
|
||||
self.fatal("Failed to get list: " + key + ": " + err);
|
||||
}
|
||||
|
||||
self.copyKey( key ); // list header
|
||||
|
||||
for (var page_idx = list.first_page; page_idx <= list.last_page; page_idx++) {
|
||||
self.copyKey( key + '/' + page_idx );
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
// copy record
|
||||
if (this.newStorage.isBinaryKey(key)) {
|
||||
// binary record, use streams
|
||||
this.oldStorage.getStream(key, function(err, stream) {
|
||||
if (err) {
|
||||
if (task.ignore) return callback();
|
||||
self.fatal("Failed to getStream key: " + key + ": " + err);
|
||||
}
|
||||
|
||||
if (args.dryrun) {
|
||||
stream.on('end', function() {
|
||||
verbose("DRY RUN: Copied binary record: " + key + "\n");
|
||||
self.numRecords++;
|
||||
callback();
|
||||
});
|
||||
stream.resume();
|
||||
return;
|
||||
}
|
||||
|
||||
self.newStorage.putStream(key, stream, function(err) {
|
||||
if (err) {
|
||||
if (task.ignore) return callback();
|
||||
self.fatal("Failed to putStream key: " + key + ": " + err);
|
||||
}
|
||||
|
||||
verbose("Copied binary record: " + key + "\n");
|
||||
if (args.dots) print(".");
|
||||
self.numRecords++;
|
||||
|
||||
if (self.queueMax) {
|
||||
var queueCurrent = self.newStorage.queue.length();
|
||||
cli.progress.update( self.queueMax - queueCurrent );
|
||||
}
|
||||
|
||||
callback();
|
||||
}); // putStream
|
||||
} ); // getStream
|
||||
}
|
||||
else {
|
||||
// standard JSON record
|
||||
this.oldStorage.get(key, function(err, data) {
|
||||
if (err) {
|
||||
if (task.ignore) return callback();
|
||||
self.fatal("Failed to get key: " + key + ": " + err);
|
||||
}
|
||||
|
||||
if (args.dryrun) {
|
||||
verbose("DRY RUN: Copied record: " + key + "\n");
|
||||
self.numRecords++;
|
||||
return callback();
|
||||
}
|
||||
|
||||
self.newStorage.put(key, data, function(err) {
|
||||
if (err) {
|
||||
if (task.ignore) return callback();
|
||||
self.fatal("Failed to put key: " + key + ": " + err);
|
||||
}
|
||||
|
||||
verbose("Copied record: " + key + "\n");
|
||||
if (args.dots) print(".");
|
||||
self.numRecords++;
|
||||
|
||||
if (self.queueMax) {
|
||||
var queueCurrent = self.newStorage.queue.length();
|
||||
cli.progress.update( self.queueMax - queueCurrent );
|
||||
}
|
||||
|
||||
callback();
|
||||
}); // put
|
||||
} ); // get
|
||||
}
|
||||
break;
|
||||
} // switch copy_type
|
||||
},
|
||||
|
||||
logDebug: function(level, msg, data) {
|
||||
this.logger.debug( level, msg, data );
|
||||
},
|
||||
|
||||
logPrint: function(level, msg, data) {
|
||||
// echo message to console and log it
|
||||
switch (level) {
|
||||
case 1: print( bold.yellow(msg) + "\n" ); break;
|
||||
case 2: print( cyan(msg) + "\n" ); break;
|
||||
case 3: print( green(msg) + "\n" ); break;
|
||||
case 4: print( magenta(msg) + "\n" ); break;
|
||||
case 9: print( gray(msg) + "\n" ); break;
|
||||
default: print( msg + "\n" ); break;
|
||||
}
|
||||
if (data) print( gray( JSON.stringify(data) ) + "\n" );
|
||||
this.logger.debug( level, msg, data );
|
||||
},
|
||||
|
||||
fatal: function(msg) {
|
||||
// log fatal error and die
|
||||
this.logger.error('fatal', msg);
|
||||
die( "\n" + bold.red("ERROR: ") + bold(msg) + "\n\n" );
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
StorageMigrator.run();
|
171
workspaces/base-workspace/Cronicle-0.8.61/bin/test-plugin.js
Executable file
|
@ -0,0 +1,171 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Test Plugin for Cronicle
|
||||
var js = require('fs');
|
||||
var JSONStream = require('pixl-json-stream');
|
||||
var Logger = require('pixl-logger');
|
||||
var Tools = require('pixl-tools');
|
||||
var Perf = require('pixl-perf');
|
||||
|
||||
var perf = new Perf();
|
||||
perf.setScale( 1 ); // seconds
|
||||
perf.begin();
|
||||
|
||||
// setup stdin / stdout streams
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdout.setEncoding('utf8');
|
||||
|
||||
console.warn("Printed this with console.warn, should go to stderr, and thus straight to our logfile.");
|
||||
console.log("Printed this with console.log, should be ignored as not json, and also end up in our logfile.");
|
||||
|
||||
if (process.argv.length > 2) console.log("ARGV: " + JSON.stringify(process.argv));
|
||||
|
||||
/*process.on('SIGTERM', function() {
|
||||
console.warn("Caught SIGTERM and ignoring it! Hahahahaha!");
|
||||
} );*/
|
||||
|
||||
var stream = new JSONStream( process.stdin, process.stdout );
|
||||
stream.on('json', function(job) {
|
||||
// got job from parent
|
||||
var columns = ['hires_epoch', 'date', 'hostname', 'category', 'code', 'msg', 'data'];
|
||||
var logger = new Logger( job.log_file, columns );
|
||||
logger.set('hostname', job.hostname);
|
||||
// logger.set('component', job.id);
|
||||
logger.set('debugLevel', 9);
|
||||
|
||||
logger.debug(1, "This is a test debug log entry");
|
||||
logger.debug(9, "Here is our job, delivered via JSONStream:", job);
|
||||
logger.debug(9, "The current date/time for our job is: " + (new Date(job.now * 1000)).toString() );
|
||||
|
||||
// use some memory so we show up on the mem graph
|
||||
var buf = null;
|
||||
if (job.params.burn) {
|
||||
buf = Buffer.alloc( 1024 * 1024 * Math.floor( 128 + (Math.random() * 128) ) );
|
||||
}
|
||||
|
||||
var start = Tools.timeNow();
|
||||
var idx = 0;
|
||||
var duration = 0;
|
||||
|
||||
if (job.params.duration.toString().match(/^(\d+)\-(\d+)$/)) {
|
||||
var low = RegExp.$1;
|
||||
var high = RegExp.$2;
|
||||
low = parseInt(low);
|
||||
high = parseInt(high);
|
||||
duration = Math.round( low + (Math.random() * (high - low)) );
|
||||
logger.debug(9, "Chosen random duration: " + duration + " seconds");
|
||||
}
|
||||
else {
|
||||
duration = parseInt( job.params.duration );
|
||||
}
|
||||
|
||||
var timer = setInterval( function() {
|
||||
var now = Tools.timeNow();
|
||||
var elapsed = now - start;
|
||||
var progress = Math.min( elapsed / duration, 1.0 );
|
||||
|
||||
if (buf) buf.fill( String.fromCharCode( Math.floor( Math.random() * 256 ) ) );
|
||||
|
||||
if (job.params.progress) {
|
||||
// report progress
|
||||
logger.debug(9, "Progress: " + progress);
|
||||
stream.write({
|
||||
progress: progress
|
||||
});
|
||||
}
|
||||
|
||||
idx++;
|
||||
if (idx % 10 == 0) {
|
||||
logger.debug(9, "Now is the ⏱ for all good 🏃 to come to the 🏥 of their 🇺🇸! " + progress);
|
||||
}
|
||||
|
||||
if (progress >= 1.0) {
|
||||
logger.debug(9, "We're done!");
|
||||
perf.end();
|
||||
clearTimeout( timer );
|
||||
|
||||
// insert some fake random stats into perf
|
||||
var max = perf.scale * (duration / 5);
|
||||
var rand_range = function(low, high) { return low + (Math.random() * (high - low)); };
|
||||
|
||||
perf.perf.db_query = { end: 1, elapsed: rand_range(0, max * 0.3) };
|
||||
perf.perf.db_connect = { end: 1, elapsed: rand_range(max * 0.2, max * 0.5) };
|
||||
perf.perf.log_read = { end: 1, elapsed: rand_range(max * 0.4, max * 0.7) };
|
||||
perf.perf.gzip_data = { end: 1, elapsed: rand_range(max * 0.6, max * 0.9) };
|
||||
perf.perf.http_post = { end: 1, elapsed: rand_range(max * 0.8, max * 1) };
|
||||
|
||||
// include a table with some stats
|
||||
var table = {
|
||||
title: "Sample Job Stats",
|
||||
header: [
|
||||
"IP Address", "DNS Lookup", "Flag", "Count", "Percentage"
|
||||
],
|
||||
rows: [
|
||||
["62.121.210.2", "directing.com", "MaxEvents-ImpsUserHour-DMZ", 138, "0.0032%" ],
|
||||
["97.247.105.50", "hsd2.nm.comcast.net", "MaxEvents-ImpsUserHour-ILUA", 84, "0.0019%" ],
|
||||
["21.153.110.51", "grandnetworks.net", "InvalidIP-Basic", 20, "0.00046%" ],
|
||||
["95.224.240.69", "hsd6.mi.comcast.net", "MaxEvents-ImpsUserHour-NM", 19, "0.00044%" ],
|
||||
["72.129.60.245", "hsd6.nm.comcast.net", "InvalidCat-Domestic", 17, "0.00039%" ],
|
||||
["21.239.78.116", "cable.mindsprung.com", "InvalidDog-Exotic", 15, "0.00037%" ],
|
||||
["172.24.147.27", "cliento.mchsi.com", "MaxEvents-ClicksPer", 14, "0.00035%" ],
|
||||
["60.203.211.33", "rgv.res.com", "InvalidFrog-Croak", 14, "0.00030%" ],
|
||||
["24.8.8.129", "dsl.att.com", "Pizza-Hawaiian", 12, "0.00025%" ],
|
||||
["255.255.1.1", "favoriteisp.com", "Random-Data", 10, "0%" ]
|
||||
],
|
||||
caption: "This is an example stats table you can generate from within your Plugin code."
|
||||
};
|
||||
|
||||
// include a custom html report
|
||||
var html = {
|
||||
title: "Sample Job Report",
|
||||
content: "<pre>This is a sample text report you can generate from within your Plugin code (can be HTML too).\n\n-------------------------------------------------\n Date/Time | 2015-10-01 6:28:38 AM \n Elapsed Time | 1 hour 15 minutes \n Total Log Rows | 4,313,619 \n Skipped Rows | 15 \n Pre-Filtered Rows | 16,847 \n Events | 4,296,757 \n Impressions | 4,287,421 \n Backup Impressions | 4,000 \n Clicks | 5,309 (0.12%) \n Backup Clicks | 27 (0.00062%) \n Unique Users | 1,239,502 \n Flagged Users | 1,651 \n Ignored Users | 1,025,910 \n Other Users | 211,941 \n Flagged Events | 6,575 (0.15%) \nFlagged Impressions | 6,327 (0.14%) \n Flagged Clicks | 241 (4.53%) \n Memory Usage | 7.38 GB \n-------------------------------------------------</pre>",
|
||||
caption: ""
|
||||
};
|
||||
|
||||
switch (job.params.action) {
|
||||
case 'Success':
|
||||
logger.debug(9, "Simulating a successful response");
|
||||
stream.write({
|
||||
complete: 1,
|
||||
code: 0,
|
||||
description: "Success!",
|
||||
perf: perf.summarize(),
|
||||
table: table,
|
||||
html: html
|
||||
});
|
||||
break;
|
||||
|
||||
case 'Failure':
|
||||
logger.debug(9, "Simulating a failure response");
|
||||
stream.write({
|
||||
complete: 1,
|
||||
code: 999,
|
||||
description: "Simulating an error message here. Something went wrong!",
|
||||
perf: perf.summarize()
|
||||
});
|
||||
break;
|
||||
|
||||
case 'Crash':
|
||||
logger.debug(9, "Simulating a crash");
|
||||
setTimeout( function() {
|
||||
// process.exit(1);
|
||||
throw new Error("Test Crash");
|
||||
}, 100 );
|
||||
break;
|
||||
}
|
||||
|
||||
// process.exit(0);
|
||||
}
|
||||
else {
|
||||
// burn up some CPU so we show up on the chart
|
||||
if (job.params.burn) {
|
||||
var temp = Tools.timeNow();
|
||||
while (Tools.timeNow() - temp < 0.10) {
|
||||
var x = Math.PI * 32768 / 100.3473847384 * Math.random();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}, 150 );
|
||||
|
||||
} );
|
174
workspaces/base-workspace/Cronicle-0.8.61/bin/url-plugin.js
Executable file
|
@ -0,0 +1,174 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// URL Plugin for Cronicle
|
||||
// Invoked via the 'HTTP Client' Plugin
|
||||
// Copyright (c) 2017 Joseph Huckaby
|
||||
// Released under the MIT License
|
||||
|
||||
// Job Params:
|
||||
// method, url, headers, data, timeout, follow, ssl_cert_bypass, success_match, error_match
|
||||
|
||||
var fs = require('fs');
|
||||
var os = require('os');
|
||||
var cp = require('child_process');
|
||||
var path = require('path');
|
||||
var JSONStream = require('pixl-json-stream');
|
||||
var Tools = require('pixl-tools');
|
||||
var Request = require('pixl-request');
|
||||
|
||||
// setup stdin / stdout streams
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdout.setEncoding('utf8');
|
||||
|
||||
var stream = new JSONStream( process.stdin, process.stdout );
|
||||
stream.on('json', function(job) {
|
||||
// got job from parent
|
||||
var params = job.params;
|
||||
var request = new Request();
|
||||
|
||||
var print = function(text) {
|
||||
fs.appendFileSync( job.log_file, text );
|
||||
};
|
||||
|
||||
// timeout
|
||||
request.setTimeout( (params.timeout || 0) * 1000 );
|
||||
|
||||
// ssl cert bypass
|
||||
if (params.ssl_cert_bypass) {
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||
}
|
||||
|
||||
if (!params.url || !params.url.match(/^https?\:\/\/\S+$/i)) {
|
||||
stream.write({ complete: 1, code: 1, description: "Malformed URL: " + (params.url || '(n/a)') });
|
||||
return;
|
||||
}
|
||||
|
||||
// allow URL to be substituted using [placeholders]
|
||||
params.url = Tools.sub( params.url, job );
|
||||
|
||||
print("Sending HTTP " + params.method + " to URL:\n" + params.url + "\n");
|
||||
|
||||
// headers
|
||||
if (params.headers) {
|
||||
// allow headers to be substituted using [placeholders]
|
||||
params.headers = Tools.sub( params.headers, job );
|
||||
|
||||
print("\nRequest Headers:\n" + params.headers.trim() + "\n");
|
||||
params.headers.replace(/\r\n/g, "\n").trim().split(/\n/).forEach( function(pair) {
|
||||
if (pair.match(/^([^\:]+)\:\s*(.+)$/)) {
|
||||
request.setHeader( RegExp.$1, RegExp.$2 );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
// follow redirects
|
||||
if (params.follow) request.setFollow( 32 );
|
||||
|
||||
var opts = {
|
||||
method: params.method
|
||||
};
|
||||
|
||||
// post data
|
||||
if (opts.method == 'POST') {
|
||||
// allow POST data to be substituted using [placeholders]
|
||||
params.data = Tools.sub( params.data, job );
|
||||
|
||||
print("\nPOST Data:\n" + params.data.trim() + "\n");
|
||||
opts.data = Buffer.from( params.data || '' );
|
||||
}
|
||||
|
||||
// matching
|
||||
var success_match = new RegExp( params.success_match || '.*' );
|
||||
var error_match = new RegExp( params.error_match || '(?!)' );
|
||||
|
||||
// send request
|
||||
request.request( params.url, opts, function(err, resp, data, perf) {
|
||||
// HTTP code out of success range = error
|
||||
if (!err && ((resp.statusCode < 200) || (resp.statusCode >= 400))) {
|
||||
err = new Error("HTTP " + resp.statusCode + " " + resp.statusMessage);
|
||||
err.code = resp.statusCode;
|
||||
}
|
||||
|
||||
// successmatch? errormatch?
|
||||
var text = data ? data.toString() : '';
|
||||
if (!err) {
|
||||
if (text.match(error_match)) {
|
||||
err = new Error("Response contains error match: " + params.error_match);
|
||||
}
|
||||
else if (!text.match(success_match)) {
|
||||
err = new Error("Response missing success match: " + params.success_match);
|
||||
}
|
||||
}
|
||||
|
||||
// start building cronicle JSON update
|
||||
var update = {
|
||||
complete: 1
|
||||
};
|
||||
if (err) {
|
||||
update.code = err.code || 1;
|
||||
update.description = err.message || err;
|
||||
}
|
||||
else {
|
||||
update.code = 0;
|
||||
update.description = "Success (HTTP " + resp.statusCode + " " + resp.statusMessage + ")";
|
||||
}
|
||||
|
||||
print( "\n" + update.description + "\n" );
|
||||
|
||||
// add raw response headers into table
|
||||
if (resp && resp.rawHeaders) {
|
||||
var rows = [];
|
||||
print("\nResponse Headers:\n");
|
||||
|
||||
for (var idx = 0, len = resp.rawHeaders.length; idx < len; idx += 2) {
|
||||
rows.push([ resp.rawHeaders[idx], resp.rawHeaders[idx + 1] ]);
|
||||
print( resp.rawHeaders[idx] + ": " + resp.rawHeaders[idx + 1] + "\n" );
|
||||
}
|
||||
|
||||
update.table = {
|
||||
title: "HTTP Response Headers",
|
||||
header: ["Header Name", "Header Value"],
|
||||
rows: rows.sort( function(a, b) {
|
||||
return a[0].localeCompare(b[0]);
|
||||
} )
|
||||
};
|
||||
}
|
||||
|
||||
// add response headers to chain_data if applicable
|
||||
if (job.chain) {
|
||||
update.chain_data = {
|
||||
headers: resp.headers
|
||||
};
|
||||
}
|
||||
|
||||
// add raw response content, if text (and not too long)
|
||||
if (text && resp.headers['content-type'] && resp.headers['content-type'].match(/(text|javascript|json|css|html)/i)) {
|
||||
print("\nRaw Response Content:\n" + text.trim() + "\n");
|
||||
|
||||
if (text.length < 32768) {
|
||||
update.html = {
|
||||
title: "Raw Response Content",
|
||||
content: "<pre>" + text.replace(/</g, '<').trim() + "</pre>"
|
||||
};
|
||||
}
|
||||
|
||||
// if response was JSON and chain mode is enabled, chain parsed data
|
||||
if (job.chain && (text.length < 1024 * 1024) && resp.headers['content-type'].match(/(application|text)\/json/i)) {
|
||||
var json = null;
|
||||
try { json = JSON.parse(text); }
|
||||
catch (e) {
|
||||
print("\nWARNING: Failed to parse JSON response: " + e + " (could not include JSON in chain_data)\n");
|
||||
}
|
||||
if (json) update.chain_data.json = json;
|
||||
}
|
||||
}
|
||||
|
||||
if (perf) {
|
||||
// passthru perf to cronicle
|
||||
update.perf = perf.metrics();
|
||||
print("\nPerformance Metrics: " + perf.summarize() + "\n");
|
||||
}
|
||||
|
||||
stream.write(update);
|
||||
} );
|
||||
});
|
10
workspaces/base-workspace/Cronicle-0.8.61/htdocs/blank.html
Executable file
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Blank</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
585
workspaces/base-workspace/Cronicle-0.8.61/htdocs/css/style.css
Executable file
|
@ -0,0 +1,585 @@
|
|||
/* Styles for Cronicle */
|
||||
/* Theme colors: #3f7ed5, #5890db, #7cafda, #9ccffa */
|
||||
|
||||
div.container {
|
||||
min-width: 750px;
|
||||
}
|
||||
|
||||
#d_header_logo {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: url(/images/clock-bkgnd.png) no-repeat center center;
|
||||
background-size: 36px 36px;
|
||||
}
|
||||
|
||||
.header_clock_layer {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: 36px 36px;
|
||||
opacity: 0;
|
||||
|
||||
transform-origin: 50% 50%;
|
||||
-webkit-transform-origin: 50% 50%;
|
||||
|
||||
transform: rotateZ(0deg);
|
||||
-webkit-transform: rotateZ(0deg);
|
||||
|
||||
/* transition: all 0.5s ease-in-out;
|
||||
-webkit-transition: all 0.5s ease-in-out; */
|
||||
}
|
||||
|
||||
#d_header_clock_hour {
|
||||
background-image: url(/images/clock-hour.png);
|
||||
}
|
||||
|
||||
#d_header_clock_minute {
|
||||
background-image: url(/images/clock-minute.png);
|
||||
}
|
||||
|
||||
#d_header_clock_second {
|
||||
background-image: url(/images/clock-second.png);
|
||||
}
|
||||
|
||||
#d_tab_time {
|
||||
cursor: default;
|
||||
opacity: 0;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
#d_tab_master {
|
||||
cursor: default;
|
||||
}
|
||||
#d_tab_master.active {
|
||||
cursor: pointer;
|
||||
}
|
||||
#d_tab_master.active:hover {
|
||||
color: #036 !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#d_scroll_time {
|
||||
position: fixed;
|
||||
box-sizing: border-box;
|
||||
top: -30px;
|
||||
left: 100%;
|
||||
margin-left: -180px;
|
||||
width: 180px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
|
||||
background: #f8f8f8;
|
||||
border-left: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
color: #777;
|
||||
text-shadow: #fff 1px 1px;
|
||||
box-shadow: rgba(0,0,0,0.05) 0px 2px 3px;
|
||||
|
||||
z-index: 10003;
|
||||
}
|
||||
|
||||
/* Menus */
|
||||
|
||||
.subtitle_menu {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
border: none;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #999;
|
||||
background-color: white;
|
||||
|
||||
max-width: 100px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.subtitle_menu:hover {
|
||||
color: #3f7ed5;
|
||||
}
|
||||
|
||||
/* Material Design Icons Additions */
|
||||
|
||||
.mdi-lg {
|
||||
/* Larger MD Icon */
|
||||
transform-origin: 50% 50%;
|
||||
-webkit-transform-origin: 50% 50%;
|
||||
|
||||
transform: scale(1.25);
|
||||
-webkit-transform: scale(1.25);
|
||||
}
|
||||
|
||||
/* Timing Params */
|
||||
|
||||
div.timing_details_label {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #3f7ed5;
|
||||
cursor: default;
|
||||
text-shadow: 0px 1px 0px white;
|
||||
}
|
||||
|
||||
div.timing_details_content {
|
||||
margin-top: 1px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
/* Plugin Params */
|
||||
|
||||
div.plugin_params_label {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #3f7ed5;
|
||||
cursor: default;
|
||||
text-shadow: 0px 1px 0px white;
|
||||
}
|
||||
|
||||
div.plugin_params_content {
|
||||
margin-top: 1px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
div.plugin_params_content input, div.plugin_params_content select, div.plugin_params_content label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
body.chrome div.plugin_params_content label {
|
||||
/* Chrome hack */
|
||||
position: relative;
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
.std_combo_unit_table input { font-size: 14px; }
|
||||
.std_combo_unit_table select { font-size: 12px; }
|
||||
|
||||
.fieldset_params_table td {
|
||||
font-weight: normal;
|
||||
padding-right: 6px;
|
||||
}
|
||||
.fieldset_params_table input, .fieldset_params_table select, .fieldset_params_table label {
|
||||
font-size: 12px;
|
||||
}
|
||||
.fieldset_params_table label {
|
||||
font-weight: normal;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* Pies */
|
||||
|
||||
@media only screen and (min-width: 1020px) and (max-width: 1100px) {
|
||||
div.pie-column {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 950px) and (max-width: 1020px) {
|
||||
div.pie-column {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 880px) and (max-width: 950px) {
|
||||
div.pie-column {
|
||||
transform: scale(0.7);
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 880px) {
|
||||
div.pie-column {
|
||||
transform: scale(0.6);
|
||||
}
|
||||
}
|
||||
|
||||
div.pie-column {
|
||||
width: 321px;
|
||||
}
|
||||
div.pie-column.column-left {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
div.pie-column.column-center {
|
||||
margin: 0 auto 0 auto;
|
||||
width: 321px;
|
||||
position: relative
|
||||
}
|
||||
div.pie-column.column-right {
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
margin-left: -321px;
|
||||
}
|
||||
|
||||
div.pie-title {
|
||||
width: 250px;
|
||||
height: 20px;
|
||||
line-height: 15px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
color: #3f7ed5;
|
||||
}
|
||||
|
||||
canvas.pie {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
div.pie-overlay {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
z-index: 2
|
||||
}
|
||||
|
||||
div.pie-overlay-title {
|
||||
margin-top: 103px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.pie-overlay-subtitle {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
div.pie-legend-column {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 66px;
|
||||
height: 250px;
|
||||
margin-left: 5px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
div.pie-legend-container {
|
||||
width: 66px;
|
||||
}
|
||||
|
||||
div.pie-legend-item {
|
||||
width: 60px;
|
||||
height: 11px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
border-radius: 4px;
|
||||
padding: 3px 3px 3px 3px;
|
||||
color: white;
|
||||
text-shadow: 0px 1px 1px black;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Line Charts */
|
||||
|
||||
div.graph-title {
|
||||
height: 20px;
|
||||
line-height: 15px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
color: #3f7ed5;
|
||||
}
|
||||
|
||||
.c3-line { stroke-width: 2px !important; }
|
||||
.c3-legend-item { font-size: 13px !important; }
|
||||
.c3-axis { font-size: 12px !important; fill: #888 !important; }
|
||||
.c3-axis path { stroke: #ccc !important; }
|
||||
.c3-axis .tick line { stroke: #ccc !important; }
|
||||
.c3-grid line { stroke: #ccc !important; }
|
||||
|
||||
/* Live Log Tail */
|
||||
|
||||
pre.log_chunk {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Data Table Row Colors */
|
||||
|
||||
.data_table tr.plain td, .swatch.plain {
|
||||
background-color: #efefef;
|
||||
/* background-image: linear-gradient(to top, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%); */
|
||||
}
|
||||
.data_table tr.red td, .swatch.red {
|
||||
background-color: #ffe0e0;
|
||||
/* background-image: linear-gradient(to top, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%); */
|
||||
}
|
||||
.data_table tr.green td, .swatch.green {
|
||||
background-color: #d8ffd8;
|
||||
/* background-image: linear-gradient(to top, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%); */
|
||||
}
|
||||
.data_table tr.blue td, .swatch.blue {
|
||||
background-color: #e0f0ff;
|
||||
/* background-image: linear-gradient(to top, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%); */
|
||||
}
|
||||
.data_table tr.skyblue td, .swatch.skyblue {
|
||||
background-color: #e0ffff;
|
||||
/* background-image: linear-gradient(to top, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%); */
|
||||
}
|
||||
.data_table tr.yellow td, .swatch.yellow {
|
||||
background-color: #ffffd0;
|
||||
/* background-image: linear-gradient(to top, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%); */
|
||||
}
|
||||
.data_table tr.purple td, .swatch.purple {
|
||||
background-color: #ffe0ff;
|
||||
/* background-image: linear-gradient(to top, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%); */
|
||||
}
|
||||
.data_table tr.orange td, .swatch.orange {
|
||||
background-color: #ffe8d0;
|
||||
/* background-image: linear-gradient(to top, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%); */
|
||||
}
|
||||
|
||||
/* Misc */
|
||||
|
||||
div.activity_desc > b {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.link {
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.link.addme {
|
||||
/*font-weight: normal !important;*/
|
||||
font-weight: bold;
|
||||
color: #777;
|
||||
margin-left: 5px;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
.link.addme:hover {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
.swatch {
|
||||
float: left;
|
||||
width: 50px;
|
||||
height: 20px;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 5px;
|
||||
margin: 1px;
|
||||
/* background-image: linear-gradient(to top, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.5) 100%) !important; */
|
||||
cursor: pointer;
|
||||
}
|
||||
.swatch:hover {
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
.swatch.active {
|
||||
border: 1px solid #888;
|
||||
}
|
||||
|
||||
tr.collapse {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.schedule_group_header {
|
||||
margin-top: 11px;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
div.schedule_group_button_container > i {
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
/* padding: 2px;
|
||||
box-shadow: 0px 0px 0px 1px #ccc; */
|
||||
}
|
||||
div.schedule_group_button_container > i:hover {
|
||||
color:#036;
|
||||
}
|
||||
div.schedule_group_button_container > i.selected {
|
||||
color: #3f7ed5;
|
||||
cursor: default !important;
|
||||
}
|
||||
div.schedule_group_button_container > i.selected:hover {
|
||||
color: #3f7ed5 !important;
|
||||
}
|
||||
|
||||
/* Password Stuff */
|
||||
|
||||
.password_toggle {
|
||||
display: inline-block;
|
||||
width: 32px;
|
||||
line-height: 15px;
|
||||
font-size: 11px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.psi_container {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin: 2px 0px 2px 0px;
|
||||
height: 8px;
|
||||
border: 1px solid #eee;
|
||||
cursor: pointer;
|
||||
}
|
||||
body.safari div.psi_container {
|
||||
/* Safari has a weird margin issue with this thing */
|
||||
margin-left: 2px !important;
|
||||
}
|
||||
|
||||
.psi_bar {
|
||||
box-sizing: border-box;
|
||||
width: 0%;
|
||||
height: 6px;
|
||||
transition: all 0.5s ease;
|
||||
-webkit-transition: all 0.5s ease;
|
||||
}
|
||||
.psi_bar.str0 {
|
||||
width: 20%;
|
||||
background: red;
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
.psi_bar.str1 {
|
||||
width: 40%;
|
||||
background: red;
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
.psi_bar.str2 {
|
||||
width: 60%;
|
||||
background: yellow;
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
.psi_bar.str3 {
|
||||
width: 80%;
|
||||
background: rgb(0, 255, 0);
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
.psi_bar.str4 {
|
||||
width: 100%;
|
||||
background: rgb(0, 255, 0);
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Slider */
|
||||
|
||||
input[type=range] {
|
||||
-webkit-appearance: none;
|
||||
margin: 10px 0;
|
||||
width: 100%;
|
||||
}
|
||||
input[type=range]:focus {
|
||||
outline: none;
|
||||
}
|
||||
input[type=range]::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
cursor: pointer;
|
||||
background: #ccc;
|
||||
background-image: linear-gradient(to bottom, #eee 0%, #ccc 100%);
|
||||
box-shadow: 1px 1px 1px #aaa inset, -1px -1px 1px #eee inset;
|
||||
border-radius: 5px;
|
||||
}
|
||||
input[type=range]::-webkit-slider-thumb {
|
||||
border: 0px solid #000000;
|
||||
height: 20px;
|
||||
width: 24px;
|
||||
border-radius: 10px;
|
||||
background: #ccc;
|
||||
background-image: linear-gradient(to bottom, #ddd 0%, #999 100%);
|
||||
box-shadow: 1px 1px 0px #eee inset, -1px -1px 0px #999 inset;
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
input[type=range]::-moz-range-track {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
cursor: pointer;
|
||||
background: #ccc;
|
||||
background-image: linear-gradient(to bottom, #eee 0%, #ccc 100%);
|
||||
box-shadow: 1px 1px 1px #aaa inset, -1px -1px 1px #eee inset;
|
||||
border-radius: 5px;
|
||||
}
|
||||
input[type=range]::-moz-range-thumb {
|
||||
border: 0px solid #000000;
|
||||
height: 20px;
|
||||
width: 24px;
|
||||
border-radius: 10px;
|
||||
background: #ccc;
|
||||
background-image: linear-gradient(to bottom, #ddd 0%, #999 100%);
|
||||
box-shadow: 1px 1px 0px #eee inset, -1px -1px 0px #999 inset;
|
||||
cursor: pointer;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
/* Date/Time Dialog */
|
||||
|
||||
fieldset.dt_fs {
|
||||
margin-bottom: 4px;
|
||||
padding: 2px 5px 5px 5px;
|
||||
}
|
||||
fieldset.dt_fs > legend {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Table Pagination Spacing Fix */
|
||||
|
||||
div.pagination > table tr td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Subtitle Widget Adjustments */
|
||||
|
||||
.subtitle_widget a, .subtitle_widget span.link {
|
||||
color: #777;
|
||||
text-decoration: none;
|
||||
}
|
||||
.subtitle_widget a:hover, .subtitle_widget span.link:hover {
|
||||
text-decoration: underline;
|
||||
color: #036;
|
||||
}
|
||||
#s_watch_job:hover {
|
||||
color: #036 !important;
|
||||
}
|
||||
|
||||
span.link.abort {
|
||||
color: #777;
|
||||
}
|
||||
span.link.abort:hover {
|
||||
color: red;
|
||||
}
|
||||
|
||||
/* Dialog Fixes */
|
||||
|
||||
div.dialog_subtitle {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Cursor fix */
|
||||
|
||||
td.table_label {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* User Category/Group Privilege Checkbox Label Augmentation */
|
||||
|
||||
#fe_eu_priv_cat_limit:checked + label:after {
|
||||
content: ':'
|
||||
}
|
||||
#fe_eu_priv_grp_limit:checked + label:after {
|
||||
content: ':'
|
||||
}
|
BIN
workspaces/base-workspace/Cronicle-0.8.61/htdocs/favicon.ico
Executable file
After Width: | Height: | Size: 5.3 KiB |
BIN
workspaces/base-workspace/Cronicle-0.8.61/htdocs/images/clock-bkgnd.png
Executable file
After Width: | Height: | Size: 7.9 KiB |
BIN
workspaces/base-workspace/Cronicle-0.8.61/htdocs/images/clock-hour.png
Executable file
After Width: | Height: | Size: 2.1 KiB |
BIN
workspaces/base-workspace/Cronicle-0.8.61/htdocs/images/clock-minute.png
Executable file
After Width: | Height: | Size: 2.1 KiB |
BIN
workspaces/base-workspace/Cronicle-0.8.61/htdocs/images/clock-second.png
Executable file
After Width: | Height: | Size: 1.8 KiB |
BIN
workspaces/base-workspace/Cronicle-0.8.61/htdocs/images/loading-16.gif
Executable file
After Width: | Height: | Size: 1.8 KiB |
BIN
workspaces/base-workspace/Cronicle-0.8.61/htdocs/images/loading-24.gif
Executable file
After Width: | Height: | Size: 2.5 KiB |
BIN
workspaces/base-workspace/Cronicle-0.8.61/htdocs/images/loading.gif
Executable file
After Width: | Height: | Size: 2.7 KiB |
BIN
workspaces/base-workspace/Cronicle-0.8.61/htdocs/images/logo-1024.png
Executable file
After Width: | Height: | Size: 35 KiB |
BIN
workspaces/base-workspace/Cronicle-0.8.61/htdocs/images/logo-128.png
Executable file
After Width: | Height: | Size: 4.5 KiB |
BIN
workspaces/base-workspace/Cronicle-0.8.61/htdocs/images/logo-256.png
Executable file
After Width: | Height: | Size: 8.4 KiB |
BIN
workspaces/base-workspace/Cronicle-0.8.61/htdocs/images/logo-512.png
Executable file
After Width: | Height: | Size: 17 KiB |
BIN
workspaces/base-workspace/Cronicle-0.8.61/htdocs/images/logo-64.png
Executable file
After Width: | Height: | Size: 2.8 KiB |
119
workspaces/base-workspace/Cronicle-0.8.61/htdocs/index-dev.html
Executable file
|
@ -0,0 +1,119 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<title>Loading...</title>
|
||||
<meta name="description" content="A simple distributed task scheduler and runner.">
|
||||
<meta name="author" content="Joseph Huckaby">
|
||||
<link rel="shortcut icon" href="/favicon.ico">
|
||||
|
||||
<!-- BUILD: COMBINE_STYLE_START -->
|
||||
<link rel="stylesheet" href="css/base.css">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/font-awesome.min.css">
|
||||
<link rel="stylesheet" href="css/materialdesignicons.min.css">
|
||||
<!-- BUILD: COMBINE_STYLE_END -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="d_message" class="message" style="display:none" onMouseUp="app.hideMessage(250)">
|
||||
<div id="d_message_inner" class="message_inner"></div>
|
||||
</div>
|
||||
|
||||
<div id="d_scroll_time" style="opacity:0"><i class="fa fa-clock-o"> </i><span></span></div>
|
||||
|
||||
<div id="d_header">
|
||||
<div class="container">
|
||||
<div id="d_header_logo" class="left">
|
||||
<div class="header_clock_layer" id="d_header_clock_hour"></div>
|
||||
<div class="header_clock_layer" id="d_header_clock_minute"></div>
|
||||
<div class="header_clock_layer" id="d_header_clock_second"></div>
|
||||
</div>
|
||||
<div id="d_header_title" class="left"></div>
|
||||
<div id="d_header_user_container" style="right"></div>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
|
||||
<div class="master_content_container">
|
||||
<!-- Main Content Area -->
|
||||
<div class="tab_bar" style="display:none">
|
||||
<div id="tab_Login" class="tab inactive" style="display:none"><span class="content"></span></div>
|
||||
<div id="tab_Home" class="tab inactive"><span class="content"><i class="mdi mdi-home-variant mdi-lg"> </i>Home</span></div>
|
||||
<div id="tab_Schedule" class="tab inactive"><span class="content"><i class="mdi mdi-calendar-clock mdi-lg"> </i>Schedule</span></div>
|
||||
<div id="tab_History" class="tab inactive"><span class="content"><i class="fa fa-history"> </i>Completed</span></div>
|
||||
<div id="tab_JobDetails" class="tab inactive" style="display:none"><span class="content"><i class="fa fa-pie-chart"> </i>Job Details</span></div>
|
||||
<div id="tab_MyAccount" class="tab inactive"><span class="content"><i class="mdi mdi-account mdi-lg"> </i>My Account</span></div>
|
||||
<div id="tab_Admin" class="tab inactive" style="display:none"><span class="content"><i class="mdi mdi-lock mdi-lg"> </i>Admin</span></div>
|
||||
|
||||
<div id="d_tab_master" class="tab_widget" onMouseUp="app.toggleMasterSwitch()"></div>
|
||||
<div id="d_tab_time" class="tab_widget"><i class="fa fa-clock-o"> </i><span></span></div>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
|
||||
<div id="main" class="main">
|
||||
<div id="page_Home" style="display:none"></div>
|
||||
<div id="page_Schedule" style="display:none"></div>
|
||||
<div id="page_History" style="display:none"></div>
|
||||
<div id="page_JobDetails" style="display:none"></div>
|
||||
<div id="page_MyAccount" style="display:none"></div>
|
||||
<div id="page_Admin" style="display:none"></div>
|
||||
<div id="page_Login" style="display:none"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="d_footer">
|
||||
<div class="left">
|
||||
<a href="https://github.com/jhuckaby/Cronicle" target="_blank">Cronicle</a> is
|
||||
© 2015 - 2020 by <a href="http://pixlcore.com" target="_blank">PixlCore</a>.
|
||||
Released under the <a href="https://github.com/jhuckaby/Cronicle/blob/master/LICENSE.md" target="_blank">MIT License</a>.
|
||||
</div>
|
||||
<div id="d_footer_version" class="right">
|
||||
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="js/external/jquery.min.js"></script>
|
||||
<script src="js/external/moment.min.js"></script>
|
||||
<script src="js/external/moment-timezone-with-data.min.js"></script>
|
||||
<script src="js/external/Chart.min.js"></script>
|
||||
<script src="js/external/jstz.min.js"></script>
|
||||
|
||||
<!-- BUILD: COMBINE_SCRIPT_START -->
|
||||
<script src="js/common/md5.js"></script>
|
||||
<script src="js/common/oop.js"></script>
|
||||
<script src="js/common/xml.js"></script>
|
||||
<script src="js/common/tools.js"></script>
|
||||
<script src="js/common/datetime.js"></script>
|
||||
<script src="js/common/page.js"></script>
|
||||
<script src="js/common/dialog.js"></script>
|
||||
<script src="js/common/base.js"></script>
|
||||
|
||||
<script src="js/app.js"></script>
|
||||
<script src="js/pages/Base.class.js"></script>
|
||||
<script src="js/pages/Home.class.js"></script>
|
||||
<script src="js/pages/Login.class.js"></script>
|
||||
<script src="js/pages/Schedule.class.js"></script>
|
||||
<script src="js/pages/History.class.js"></script>
|
||||
<script src="js/pages/JobDetails.class.js"></script>
|
||||
<script src="js/pages/MyAccount.class.js"></script>
|
||||
<script src="js/pages/Admin.class.js"></script>
|
||||
<script src="js/pages/admin/Categories.js"></script>
|
||||
<script src="js/pages/admin/Servers.js"></script>
|
||||
<script src="js/pages/admin/Users.js"></script>
|
||||
<script src="js/pages/admin/Plugins.js"></script>
|
||||
<script src="js/pages/admin/Activity.js"></script>
|
||||
<script src="js/pages/admin/APIKeys.js"></script>
|
||||
<!-- BUILD: COMBINE_SCRIPT_END -->
|
||||
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script src="/api/app/config?callback=app.receiveConfig"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
1146
workspaces/base-workspace/Cronicle-0.8.61/htdocs/js/app.js
Executable file
113
workspaces/base-workspace/Cronicle-0.8.61/htdocs/js/home-worker.js
Executable file
|
@ -0,0 +1,113 @@
|
|||
// Cronicle App
|
||||
// Author: Joseph Huckaby
|
||||
// Copyright (c) 2015 - 2018 Joseph Huckaby and PixlCore.com
|
||||
// MIT License
|
||||
|
||||
// Worker thread for the Home tab
|
||||
// Processes schedule to predict upcoming jobs
|
||||
|
||||
var window = {};
|
||||
|
||||
importScripts(
|
||||
'external/moment.min.js',
|
||||
'external/moment-timezone-with-data.min.js',
|
||||
'common/xml.js',
|
||||
'common/tools.js',
|
||||
'common/datetime.js'
|
||||
);
|
||||
|
||||
onmessage = function(e) {
|
||||
// process schedule and cursors, find out which events run in the next 24 hours
|
||||
var data = e.data;
|
||||
var default_tz = data.default_tz;
|
||||
var schedule = data.schedule;
|
||||
var state = data.state;
|
||||
var cursors = state.cursors;
|
||||
var categories = data.categories;
|
||||
var plugins = data.plugins;
|
||||
var events = [];
|
||||
var max_events = 10000;
|
||||
|
||||
var now = normalize_time( time_now(), { sec: 0 } );
|
||||
var max_epoch = now + 86400 + 3600;
|
||||
var time_start = hires_time_now();
|
||||
|
||||
for (var idx = 0, len = schedule.length; idx < len; idx++) {
|
||||
var item = schedule[idx];
|
||||
|
||||
// if item is disabled, skip entirely
|
||||
if (!item.enabled) continue;
|
||||
|
||||
// check category for disabled flag as well
|
||||
var cat = find_object( categories, { id: item.category } );
|
||||
if (cat && !cat.enabled) continue;
|
||||
|
||||
// check plugin for disabled flag as well
|
||||
var plugin = find_object( plugins, { id: item.plugin } );
|
||||
if (plugin && !plugin.enabled) continue;
|
||||
|
||||
// start at item cursor
|
||||
var min_epoch = (cursors[item.id] || now) + 60;
|
||||
|
||||
// if item is not in catch-up mode, force cursor to now + 60
|
||||
if (!item.catch_up) min_epoch = now + 60;
|
||||
|
||||
// setup moment, and floor to the hour
|
||||
var margs = moment.tz(min_epoch * 1000, item.timezone || default_tz);
|
||||
margs.minutes(0).seconds(0).milliseconds(0);
|
||||
|
||||
for (var epoch = min_epoch; epoch < max_epoch; epoch += 3600) {
|
||||
if (item.timing && check_event_hour(item.timing, margs)) {
|
||||
// item will run at least one time this hour
|
||||
// so we can use the timing.minutes to populate events directly
|
||||
var hour_start = margs.unix();
|
||||
|
||||
if (item.timing.minutes && item.timing.minutes.length) {
|
||||
// item runs on specific minutes
|
||||
for (var idy = 0, ley = item.timing.minutes.length; idy < ley; idy++) {
|
||||
var min = item.timing.minutes[idy];
|
||||
var actual = hour_start + (min * 60);
|
||||
if ((actual >= min_epoch) && (actual < max_epoch)) {
|
||||
events.push({ epoch: actual, id: item.id });
|
||||
if (events.length >= max_events) { idy = ley; epoch = max_epoch; idx = len; }
|
||||
}
|
||||
} // foreach minute
|
||||
} // individual minutes
|
||||
else {
|
||||
// item runs EVERY minute in the hour (unusual)
|
||||
for (var idy = 0; idy < 60; idy++) {
|
||||
var actual = hour_start + (idy * 60);
|
||||
if ((actual >= min_epoch) && (actual < max_epoch)) {
|
||||
events.push({ epoch: actual, id: item.id });
|
||||
if (events.length >= max_events) { idy = 60; epoch = max_epoch; idx = len; }
|
||||
}
|
||||
} // foreach minute
|
||||
} // every minute
|
||||
} // item runs in the hour
|
||||
|
||||
// advance moment.js by one hour
|
||||
margs.add( 1, "hours" );
|
||||
|
||||
// make sure we don't run amok (3s max run time)
|
||||
if (hires_time_now() - time_start >= 3.0) { epoch = max_epoch; idx = len; }
|
||||
} // foreach hour
|
||||
|
||||
} // foreach schedule item
|
||||
|
||||
postMessage(
|
||||
events.sort(
|
||||
function(a, b) { return (a.epoch < b.epoch) ? -1 : 1; }
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
function check_event_hour(timing, margs) {
|
||||
// check if event needs to run, up to the hour (do not check minute)
|
||||
if (!timing) return false;
|
||||
if (timing.hours && timing.hours.length && (timing.hours.indexOf(margs.hour()) == -1)) return false;
|
||||
if (timing.weekdays && timing.weekdays.length && (timing.weekdays.indexOf(margs.day()) == -1)) return false;
|
||||
if (timing.days && timing.days.length && (timing.days.indexOf(margs.date()) == -1)) return false;
|
||||
if (timing.months && timing.months.length && (timing.months.indexOf(margs.month() + 1) == -1)) return false;
|
||||
if (timing.years && timing.years.length && (timing.years.indexOf(margs.year()) == -1)) return false;
|
||||
return true;
|
||||
};
|
104
workspaces/base-workspace/Cronicle-0.8.61/htdocs/js/pages/Admin.class.js
Executable file
|
@ -0,0 +1,104 @@
|
|||
Class.subclass( Page.Base, "Page.Admin", {
|
||||
|
||||
usernames: null,
|
||||
default_sub: 'activity',
|
||||
|
||||
onInit: function() {
|
||||
// called once at page load
|
||||
var html = '';
|
||||
this.div.html( html );
|
||||
},
|
||||
|
||||
onActivate: function(args) {
|
||||
// page activation
|
||||
if (!this.requireLogin(args)) return true;
|
||||
|
||||
if (!args) args = {};
|
||||
if (!args.sub) args.sub = this.default_sub;
|
||||
this.args = args;
|
||||
|
||||
app.showTabBar(true);
|
||||
this.tab[0]._page_id = Nav.currentAnchor();
|
||||
|
||||
this.div.addClass('loading');
|
||||
this['gosub_'+args.sub](args);
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
onDataUpdate: function(key, value) {
|
||||
// recieved data update (websocket), see if sub-page cares about it
|
||||
switch (key) {
|
||||
case 'users':
|
||||
if (this.args.sub == 'users') this.gosub_users(this.args);
|
||||
break;
|
||||
|
||||
case 'categories':
|
||||
if (this.args.sub == 'categories') this.gosub_categories(this.args);
|
||||
break;
|
||||
|
||||
case 'server_groups':
|
||||
case 'servers':
|
||||
case 'nearby':
|
||||
if (this.args.sub == 'servers') this.gosub_servers(this.args);
|
||||
break;
|
||||
|
||||
case 'plugins':
|
||||
if (this.args.sub == 'plugins') this.gosub_plugins(this.args);
|
||||
break;
|
||||
|
||||
case 'state':
|
||||
case 'schedule':
|
||||
if (this.args.sub == 'servers') this.gosub_servers(this.args);
|
||||
break;
|
||||
|
||||
case 'api_keys':
|
||||
if (this.args.sub == 'api_keys') this.gosub_api_keys(this.args);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onStatusUpdate: function(data) {
|
||||
// received status update (websocket), update sub-page if needed
|
||||
if (data.jobs_changed && (this.args.sub == 'servers')) this.gosub_servers(this.args);
|
||||
if (data.servers_changed && (this.args.sub == 'servers')) this.gosub_servers(this.args);
|
||||
},
|
||||
|
||||
onResizeDelay: function(size) {
|
||||
// called 250ms after latest window resize
|
||||
// so we can run more expensive redraw operations
|
||||
switch (this.args.sub) {
|
||||
case 'users':
|
||||
if (this.lastUsersResp) {
|
||||
this.receive_users(this.lastUsersResp);
|
||||
}
|
||||
break;
|
||||
case 'categories':
|
||||
this.gosub_categories(this.args);
|
||||
break;
|
||||
case 'servers':
|
||||
this.gosub_servers(this.args);
|
||||
break;
|
||||
case 'plugins':
|
||||
this.gosub_plugins(this.args);
|
||||
break;
|
||||
case 'api_keys':
|
||||
if (this.lastAPIKeysResp) {
|
||||
this.receive_keys( this.lastAPIKeysResp );
|
||||
}
|
||||
break;
|
||||
case 'activity':
|
||||
if (this.lastActivityResp) {
|
||||
this.receive_activity( this.lastActivityResp );
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onDeactivate: function() {
|
||||
// called when page is deactivated
|
||||
// this.div.html( '' );
|
||||
return true;
|
||||
}
|
||||
|
||||
} );
|
426
workspaces/base-workspace/Cronicle-0.8.61/htdocs/js/pages/Base.class.js
Executable file
|
@ -0,0 +1,426 @@
|
|||
Class.subclass( Page, "Page.Base", {
|
||||
|
||||
graph_colors: ["0,0,255", "138,43,226", "0,128,0", "255,20,147", "0,191,255", "210,105,30", "100,149,237", "220,20,60", "0,139,139", "128,128,128"],
|
||||
|
||||
requireLogin: function(args) {
|
||||
// user must be logged into to continue
|
||||
var self = this;
|
||||
|
||||
if (!app.user) {
|
||||
// require login
|
||||
app.navAfterLogin = this.ID;
|
||||
if (args && num_keys(args)) app.navAfterLogin += compose_query_string(args);
|
||||
|
||||
this.div.hide();
|
||||
|
||||
var session_id = app.getPref('session_id') || '';
|
||||
if (session_id) {
|
||||
Debug.trace("User has cookie, recovering session: " + session_id);
|
||||
|
||||
app.api.post( 'user/resume_session', {
|
||||
session_id: session_id
|
||||
},
|
||||
function(resp) {
|
||||
if (resp.user) {
|
||||
Debug.trace("User Session Resume: " + resp.username + ": " + resp.session_id);
|
||||
app.hideProgress();
|
||||
app.doUserLogin( resp );
|
||||
Nav.refresh();
|
||||
}
|
||||
else {
|
||||
Debug.trace("User cookie is invalid, redirecting to login page");
|
||||
// Nav.go('Login');
|
||||
self.setPref('session_id', '');
|
||||
self.requireLogin(args);
|
||||
}
|
||||
} );
|
||||
}
|
||||
else if (app.config.external_users) {
|
||||
Debug.trace("User is not logged in, querying external user API");
|
||||
app.doExternalLogin();
|
||||
}
|
||||
else {
|
||||
Debug.trace("User is not logged in, redirecting to login page (will return to " + this.ID + ")");
|
||||
setTimeout( function() { Nav.go('Login'); }, 1 );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
isAdmin: function() {
|
||||
// return true if user is logged in and admin, false otherwise
|
||||
// Note: This is used for UI decoration ONLY -- all privileges are checked on the server
|
||||
return( app.user && app.user.privileges && app.user.privileges.admin );
|
||||
},
|
||||
|
||||
getNiceJob: function(id) {
|
||||
if (!id) return '(None)';
|
||||
if (typeof(id) == 'object') id = id.id;
|
||||
return '<div style="white-space:nowrap;"><i class="fa fa-pie-chart"> </i>' + id + '</div>';
|
||||
},
|
||||
|
||||
getNiceEvent: function(title, width) {
|
||||
if (!width) width = 500;
|
||||
if (!title) return '(None)';
|
||||
var icon_class = 'fa fa-clock-o';
|
||||
if (typeof(title) == 'object') {
|
||||
if (title.chain || title.chain_error) icon_class = 'fa fa-link';
|
||||
title = title.title;
|
||||
}
|
||||
return '<div class="ellip" style="max-width:'+width+'px;"><i class="' + icon_class + '"> </i>' + title + '</div>';
|
||||
},
|
||||
|
||||
getNiceCategory: function(cat, width) {
|
||||
if (!width) width = 500;
|
||||
if (!cat) return '(None)';
|
||||
var title = cat.title;
|
||||
if (!cat.enabled) title += ' (Disabled)';
|
||||
return '<div class="ellip" style="max-width:'+width+'px;"><i class="fa fa-folder-open-o"> </i>' + title + '</div>';
|
||||
},
|
||||
|
||||
getNiceGroup: function(group, target, width) {
|
||||
if (!width) width = 500;
|
||||
if (!group && !target) return '(None)';
|
||||
if (group) {
|
||||
var title = group.title;
|
||||
if (group.multiplex) title += ' (<i class="fa fa-bolt" title="Multiplexed"></i>)';
|
||||
return '<div class="ellip" style="max-width:'+width+'px;"><i class="mdi mdi-server-network"> </i>' + title + '</div>';
|
||||
}
|
||||
else {
|
||||
return '<div class="ellip" style="max-width:'+width+'px;" title="'+target+'"><i class="mdi mdi-desktop-tower mdi-lg"> </i>' + target.replace(/\.[\w\-]+\.\w+$/, '') + '</div>';
|
||||
}
|
||||
},
|
||||
|
||||
getNicePlugin: function(plugin, width) {
|
||||
if (!width) width = 500;
|
||||
if (!plugin) return '(None)';
|
||||
var title = plugin.title;
|
||||
if (!plugin.enabled) title += ' (Disabled)';
|
||||
return '<div class="ellip" style="max-width:'+width+'px;"><i class="fa fa-plug"> </i>' + title + '</div>';
|
||||
},
|
||||
|
||||
getNiceAPIKey: function(item, link, width) {
|
||||
if (!item) return 'n/a';
|
||||
if (!width) width = 500;
|
||||
var key = item.api_key || item.key;
|
||||
var title = item.api_title || item.title;
|
||||
|
||||
var html = '<div class="ellip" style="max-width:'+width+'px;">';
|
||||
if (link && key) html += '<a href="#Admin?sub=edit_api_key&id='+item.id+'">';
|
||||
|
||||
html += '<i class="mdi mdi-key-variant"> </i>' + title;
|
||||
|
||||
if (link && key) html += '</a>';
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
},
|
||||
|
||||
getNiceUsername: function(user, link, width) {
|
||||
if (!user) return 'n/a';
|
||||
if ((typeof(user) == 'object') && (user.key || user.api_title)) {
|
||||
return this.getNiceAPIKey(user, link, width);
|
||||
}
|
||||
if (!width) width = 500;
|
||||
var username = user.username ? user.username : user;
|
||||
if (!username || (typeof(username) != 'string')) return 'n/a';
|
||||
|
||||
var html = '<div class="ellip" style="max-width:'+width+'px;">';
|
||||
if (link) html += '<a href="#Admin?sub=edit_user&username='+username+'">';
|
||||
|
||||
html += '<i class="fa fa-user"> </i>' + username;
|
||||
|
||||
if (link) html += '</a>';
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
},
|
||||
|
||||
setGroupVisible: function(group, visible) {
|
||||
// set web groups of form fields visible or invisible,
|
||||
// according to master checkbox for each section
|
||||
var selector = 'tr.' + group + 'group';
|
||||
if (visible) {
|
||||
if ($(selector).hasClass('collapse')) {
|
||||
$(selector).hide().removeClass('collapse');
|
||||
}
|
||||
$(selector).show(250);
|
||||
}
|
||||
else $(selector).hide(250);
|
||||
|
||||
return this; // for chaining
|
||||
},
|
||||
|
||||
checkUserExists: function(pre) {
|
||||
// check if user exists, update UI checkbox
|
||||
// called after field changes
|
||||
var username = trim($('#fe_'+pre+'_username').val().toLowerCase());
|
||||
var $elem = $('#d_'+pre+'_valid');
|
||||
|
||||
if (username.match(/^[\w\-\.]+$/)) {
|
||||
// check with server
|
||||
// $elem.css('color','#444').html('<span class="fa fa-spinner fa-spin fa-lg"> </span>');
|
||||
app.api.get('app/check_user_exists', { username: username }, function(resp) {
|
||||
if (resp.user_exists) {
|
||||
// username taken
|
||||
$elem.css('color','red').html('<span class="fa fa-exclamation-triangle fa-lg"> </span>Username Taken');
|
||||
}
|
||||
else {
|
||||
// username is valid and available!
|
||||
$elem.css('color','green').html('<span class="fa fa-check-circle fa-lg"> </span>Available');
|
||||
}
|
||||
} );
|
||||
}
|
||||
else if (username.length) {
|
||||
// bad username
|
||||
$elem.css('color','red').html('<span class="fa fa-exclamation-triangle fa-lg"> </span>Bad Username');
|
||||
}
|
||||
else {
|
||||
// empty
|
||||
$elem.html('');
|
||||
}
|
||||
},
|
||||
|
||||
check_add_remove_me: function($elem) {
|
||||
// check if user's e-mail is contained in text field or not
|
||||
var value = $elem.val().toLowerCase();
|
||||
var email = app.user.email.toLowerCase();
|
||||
var regexp = new RegExp( "\\b" + escape_regexp(email) + "\\b" );
|
||||
return !!value.match(regexp);
|
||||
},
|
||||
|
||||
update_add_remove_me: function($elems) {
|
||||
// update add/remove me text based on if user's e-mail is contained in text field
|
||||
var self = this;
|
||||
|
||||
$elems.each( function() {
|
||||
var $elem = $(this);
|
||||
var $span = $elem.next();
|
||||
|
||||
if (self.check_add_remove_me($elem)) $span.html( '» Remove me' );
|
||||
else $span.html( '« Add me' );
|
||||
} );
|
||||
},
|
||||
|
||||
add_remove_me: function($elem) {
|
||||
// toggle user's e-mail in/out of text field
|
||||
var value = trim( $elem.val().replace(/\,\s*\,/g, ',').replace(/^\s*\,\s*/, '').replace(/\s*\,\s*$/, '') );
|
||||
|
||||
if (this.check_add_remove_me($elem)) {
|
||||
// remove e-mail
|
||||
var email = app.user.email.toLowerCase();
|
||||
var regexp = new RegExp( "\\b" + escape_regexp(email) + "\\b", "i" );
|
||||
value = value.replace( regexp, '' ).replace(/\,\s*\,/g, ',').replace(/^\s*\,\s*/, '').replace(/\s*\,\s*$/, '');
|
||||
$elem.val( trim(value) );
|
||||
}
|
||||
else {
|
||||
// add email
|
||||
if (value) value += ', ';
|
||||
$elem.val( value + app.user.email );
|
||||
}
|
||||
|
||||
this.update_add_remove_me($elem);
|
||||
},
|
||||
|
||||
get_custom_combo_unit_box: function(id, value, items, class_name) {
|
||||
// get HTML for custom combo text/menu, where menu defines units of measurement
|
||||
// items should be array for use in render_menu_options(), with an increasing numerical value
|
||||
if (!class_name) class_name = 'std_combo_unit_table';
|
||||
var units = 0;
|
||||
var value = parseInt( value || 0 );
|
||||
|
||||
for (var idx = items.length - 1; idx >= 0; idx--) {
|
||||
var max = items[idx][0];
|
||||
if ((value >= max) && (value % max == 0)) {
|
||||
units = max;
|
||||
value = Math.floor( value / units );
|
||||
idx = -1;
|
||||
}
|
||||
}
|
||||
if (!units) {
|
||||
// no exact match, so default to first unit in list
|
||||
units = items[0][0];
|
||||
value = Math.floor( value / units );
|
||||
}
|
||||
|
||||
return (
|
||||
'<table cellspacing="0" cellpadding="0" class="'+class_name+'"><tr>' +
|
||||
'<td style="padding:0"><input type="text" id="'+id+'" style="width:30px;" value="'+value+'"/></td>' +
|
||||
'<td style="padding:0"><select id="'+id+'_units">' + render_menu_options(items, units) + '</select></td>' +
|
||||
'</tr></table>'
|
||||
);
|
||||
},
|
||||
|
||||
get_relative_time_combo_box: function(id, value, class_name, inc_seconds) {
|
||||
// get HTML for combo textfield/menu for a relative time based input
|
||||
// provides Minutes, Hours and Days units
|
||||
var unit_items = [[60,'Minutes'], [3600,'Hours'], [86400,'Days']];
|
||||
if (inc_seconds) unit_items.unshift( [1,'Seconds'] );
|
||||
|
||||
return this.get_custom_combo_unit_box( id, value, unit_items, class_name );
|
||||
},
|
||||
|
||||
get_relative_size_combo_box: function(id, value, class_name) {
|
||||
// get HTML for combo textfield/menu for a relative size based input
|
||||
// provides MB, GB and TB units
|
||||
var TB = 1024 * 1024 * 1024 * 1024;
|
||||
var GB = 1024 * 1024 * 1024;
|
||||
var MB = 1024 * 1024;
|
||||
var KB = 1024;
|
||||
|
||||
return this.get_custom_combo_unit_box( id, value, [[KB, 'KB'], [MB,'MB'], [GB,'GB'], [TB,'TB']], class_name );
|
||||
},
|
||||
|
||||
expand_fieldset: function($span) {
|
||||
// expand neighboring fieldset, and hide click control
|
||||
var $div = $span.parent();
|
||||
var $fieldset = $div.next();
|
||||
$fieldset.show( 350 );
|
||||
$div.hide( 350 );
|
||||
},
|
||||
|
||||
collapse_fieldset: function($legend) {
|
||||
// collapse fieldset, and show click control again
|
||||
var $fieldset = $legend.parent();
|
||||
var $div = $fieldset.prev();
|
||||
$fieldset.hide( 350 );
|
||||
$div.show( 350 );
|
||||
},
|
||||
|
||||
choose_date_time: function(args) {
|
||||
// show dialog for selecting a date/time
|
||||
// args: {
|
||||
// when: default date/time (epoch or Date object, defaults to now)
|
||||
// timezone: custom timezone (defaults to app.tz)
|
||||
// title: dialog title
|
||||
// description: optional description
|
||||
// button: optional button label ("Select")
|
||||
// callback: fired when complete, passed new date/time
|
||||
// }
|
||||
var self = this;
|
||||
var html = '';
|
||||
|
||||
if (!args.when) args.when = time_now();
|
||||
if (!args.timezone) args.timezone = app.tz;
|
||||
if (!args.title) args.title = "Select Date/Time";
|
||||
if (!args.button) args.button = "Select";
|
||||
|
||||
if (args.description) {
|
||||
html += '<div style="font-size:12px; color:#777; margin-bottom:20px;">' + args.description + '</div>';
|
||||
}
|
||||
|
||||
// var dargs = get_date_args( args.when );
|
||||
var margs = moment.tz(args.when * 1000, args.timezone);
|
||||
|
||||
html += '<center><table><tr>';
|
||||
|
||||
// years
|
||||
var year_items = [];
|
||||
for (var idx = margs.year() - 10; idx <= margs.year() + 10; idx++) {
|
||||
year_items.push(idx);
|
||||
}
|
||||
html += '<td align="left"><fieldset class="dt_fs"><legend>Year</legend>';
|
||||
html += '<select id="fe_dt_year">' + render_menu_options(year_items, margs.year()) + '</select>';
|
||||
html += '</fieldset></td>';
|
||||
|
||||
// months
|
||||
html += '<td align="left"><fieldset class="dt_fs" style="margin-left:5px;"><legend>Month</legend>';
|
||||
html += '<select id="fe_dt_month">' + render_menu_options(_months, margs.month() + 1) + '</select>';
|
||||
html += '</fieldset></td>';
|
||||
|
||||
// days
|
||||
html += '<td align="left"><fieldset class="dt_fs" style="margin-left:5px;"><legend>Day</legend>';
|
||||
html += '<select id="fe_dt_day">' + render_menu_options(_days, margs.date()) + '</select>';
|
||||
html += '</fieldset></td>';
|
||||
|
||||
// hours
|
||||
var hour_items = _hour_names.map( function(value, idx) {
|
||||
return [idx, value.toUpperCase().replace(/^(\d+)(\w+)$/, '$1 $2')];
|
||||
} );
|
||||
html += '<td align="left"><fieldset class="dt_fs" style="margin-left:5px;"><legend>Hour</legend>';
|
||||
html += '<select id="fe_dt_hour">' + render_menu_options(hour_items, margs.hour()) + '</select>';
|
||||
html += '</fieldset></td>';
|
||||
|
||||
// minutes
|
||||
var min_items = [];
|
||||
for (var idx = 0; idx < 60; idx++) {
|
||||
min_items.push([ idx, (idx < 10) ? ('0'+idx) : (''+idx) ]);
|
||||
}
|
||||
html += '<td align="left"><fieldset class="dt_fs" style="margin-left:5px;"><legend>Minute</legend>';
|
||||
html += '<select id="fe_dt_minute">' + render_menu_options(min_items, margs.minute()) + '</select>';
|
||||
html += '</fieldset></td>';
|
||||
|
||||
html += '</tr>';
|
||||
html += '<tr><td align="left" colspan="5">';
|
||||
html += '<div class="caption">Timezone: ' + args.timezone + '</div>';
|
||||
html += '</td></tr>';
|
||||
html += '</table></center>';
|
||||
|
||||
app.confirm( '<i class="fa fa-calendar"> </i>' + args.title, html, args.button, function(result) {
|
||||
app.clearError();
|
||||
|
||||
if (result) {
|
||||
Dialog.hide();
|
||||
|
||||
margs.year( parseInt( $('#fe_dt_year').val() ) );
|
||||
margs.month( parseInt( $('#fe_dt_month').val() ) - 1 );
|
||||
margs.date( parseInt( $('#fe_dt_day').val() ) );
|
||||
margs.hour( parseInt( $('#fe_dt_hour').val() ) );
|
||||
margs.minute( parseInt( $('#fe_dt_minute').val() ) );
|
||||
margs.second( 0 );
|
||||
|
||||
args.callback( margs.unix() );
|
||||
}
|
||||
} ); // app.confirm
|
||||
},
|
||||
|
||||
render_target_menu_options: function(value) {
|
||||
// render menu items for server group (target)
|
||||
// including optgroups for both server group and individual servers
|
||||
var html = '';
|
||||
|
||||
var server_groups = app.server_groups.sort( function(a, b) {
|
||||
// return (b.title < a.title) ? 1 : -1;
|
||||
return a.title.toLowerCase().localeCompare( b.title.toLowerCase() );
|
||||
} )
|
||||
.filter( function(group) {
|
||||
if (!app.user.privileges.grp_limit) return true; // user is not limited by groups
|
||||
return app.hasPrivilege( 'grp_' + group.id );
|
||||
});
|
||||
|
||||
html += '<optgroup label="Groups:">' + render_menu_options(server_groups, value, false) + '</optgroup>';
|
||||
|
||||
if (find_object(server_groups, { id: value })) value = '';
|
||||
|
||||
// trim hostname suffixes
|
||||
var hostnames = hash_keys_to_array(app.servers).sort();
|
||||
if (value && !app.servers[value]) hostnames.push( value );
|
||||
|
||||
// filter hostnames by server group privilege
|
||||
if (app.user.privileges.grp_limit) {
|
||||
hostnames = hostnames.filter(function(hostname) {
|
||||
var groups = server_groups.filter( function(group) {
|
||||
return hostname.match( group.regexp );
|
||||
} );
|
||||
|
||||
// we just need one group to match, then the user has permission to target the server
|
||||
for (var idx = 0, len = groups.length; idx < len; idx++) {
|
||||
priv_id = 'grp_' + groups[idx].id;
|
||||
result = app.hasPrivilege(priv_id);
|
||||
if (result) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
} // grp_limit
|
||||
|
||||
var short_hostnames = [];
|
||||
for (var idx = 0, len = hostnames.length; idx < len; idx++) {
|
||||
short_hostnames.push([ hostnames[idx], hostnames[idx].replace(/\.[\w\-]+\.\w+$/, '') ]);
|
||||
}
|
||||
|
||||
html += '<optgroup label="Servers:">' + render_menu_options(short_hostnames, value, false) + '</optgroup>';
|
||||
return html;
|
||||
}
|
||||
|
||||
} );
|
777
workspaces/base-workspace/Cronicle-0.8.61/htdocs/js/pages/History.class.js
Executable file
|
@ -0,0 +1,777 @@
|
|||
// Cronicle History Page
|
||||
|
||||
Class.subclass( Page.Base, "Page.History", {
|
||||
|
||||
default_sub: 'history',
|
||||
|
||||
onInit: function() {
|
||||
// called once at page load
|
||||
// var html = '';
|
||||
// this.div.html( html );
|
||||
this.charts = {};
|
||||
},
|
||||
|
||||
onActivate: function(args) {
|
||||
// page activation
|
||||
if (!this.requireLogin(args)) return true;
|
||||
|
||||
if (!args) args = {};
|
||||
if (!args.sub) args.sub = this.default_sub;
|
||||
this.args = args;
|
||||
|
||||
app.showTabBar(true);
|
||||
this.tab[0]._page_id = Nav.currentAnchor();
|
||||
|
||||
this.div.addClass('loading');
|
||||
this['gosub_'+args.sub](args);
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
gosub_history: function(args) {
|
||||
// show history
|
||||
app.setWindowTitle( "History" );
|
||||
|
||||
var html = '';
|
||||
// html += '<div style="padding:5px 15px 15px 15px;">';
|
||||
html += '<div style="padding:20px 20px 30px 20px">';
|
||||
|
||||
html += '<div class="subtitle">';
|
||||
html += 'All Completed Jobs';
|
||||
// html += '<div class="subtitle_widget"><span class="link" onMouseUp="$P().refresh_user_list()"><b>Refresh</b></span></div>';
|
||||
// html += '<div class="subtitle_widget"><i class="fa fa-search"> </i><input type="text" id="fe_ul_search" size="15" placeholder="Find username..." style="border:0px;"/></div>';
|
||||
var sorted_events = app.schedule.sort( function(a, b) {
|
||||
return a.title.toLowerCase().localeCompare( b.title.toLowerCase() );
|
||||
} );
|
||||
html += '<div class="subtitle_widget"><i class="fa fa-chevron-down"> </i><select id="fe_hist_event" class="subtitle_menu" onChange="$P().jump_to_event_history()"><option value="">Filter by Event</option>' + render_menu_options( sorted_events, '', false ) + '</select></div>';
|
||||
html += '<div class="clear"></div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div id="d_history_table"></div>';
|
||||
html += '</div>'; // padding
|
||||
this.div.html( html );
|
||||
|
||||
this.get_history();
|
||||
},
|
||||
|
||||
get_history: function() {
|
||||
var args = this.args;
|
||||
if (!args.offset) args.offset = 0;
|
||||
if (!args.limit) args.limit = 25;
|
||||
app.api.post( 'app/get_history', copy_object(args), this.receive_history.bind(this) );
|
||||
},
|
||||
|
||||
receive_history: function(resp) {
|
||||
// receive page of history from server, render it
|
||||
this.lastHistoryResp = resp;
|
||||
|
||||
var html = '';
|
||||
this.div.removeClass('loading');
|
||||
|
||||
var size = get_inner_window_size();
|
||||
var col_width = Math.floor( ((size.width * 0.9) - 50) / 8 );
|
||||
|
||||
this.events = [];
|
||||
if (resp.rows) this.events = resp.rows;
|
||||
|
||||
var cols = ['Job ID', 'Event Name', 'Category', 'Plugin', 'Hostname', 'Result', 'Start Date/Time', 'Elapsed Time'];
|
||||
|
||||
var self = this;
|
||||
var num_visible_items = 0;
|
||||
|
||||
html += this.getPaginatedTable( resp, cols, 'event', function(job, idx) {
|
||||
/*var actions = [
|
||||
'<a href="#JobDetails?id='+job.id+'"><b>Job Details</b></a>',
|
||||
'<a href="#History?sub=event_history&id='+job.event+'"><b>Event History</b></a>'
|
||||
];*/
|
||||
|
||||
// suppress row view if job was deleted
|
||||
if (job.action != 'job_complete') return null;
|
||||
num_visible_items++;
|
||||
|
||||
var event = find_object( app.schedule, { id: job.event } );
|
||||
var event_link = '(None)';
|
||||
if (event && job.id) {
|
||||
event_link = '<div class="td_big"><a href="#History?sub=event_history&id='+job.event+'">' + self.getNiceEvent('<b>' + (event.title || job.event) + '</b>', col_width + 40) + '</a></div>';
|
||||
}
|
||||
else if (job.event_title) {
|
||||
event_link = self.getNiceEvent(job.event_title, col_width + 40);
|
||||
}
|
||||
|
||||
var cat = job.category ? find_object( app.categories, { id: job.category } ) : null;
|
||||
if (!cat && job.category_title) cat = { id: job.category, title: job.category_title };
|
||||
|
||||
var plugin = job.plugin ? find_object( app.plugins, { id: job.plugin } ) : null;
|
||||
if (!plugin && job.plugin_title) plugin = { id: job.plugin, title: job.plugin_title };
|
||||
|
||||
var job_link = '<div class="td_big">--</div>';
|
||||
if (job.id) job_link = '<div class="td_big"><a href="#JobDetails?id='+job.id+'">' + self.getNiceJob('<b>' + job.id + '</b>') + '</a></div>';
|
||||
|
||||
var tds = [
|
||||
job_link,
|
||||
event_link,
|
||||
self.getNiceCategory( cat, col_width ),
|
||||
self.getNicePlugin( plugin, col_width ),
|
||||
self.getNiceGroup( null, job.hostname, col_width ),
|
||||
(job.code == 0) ? '<span class="color_label green"><i class="fa fa-check"> </i>Success</span>' : '<span class="color_label red"><i class="fa fa-warning"> </i>Error</span>',
|
||||
get_nice_date_time( job.time_start, false, true ),
|
||||
get_text_from_seconds( job.elapsed, true, false )
|
||||
// actions.join(' | ')
|
||||
];
|
||||
|
||||
if (!job.id) tds.className = 'disabled';
|
||||
|
||||
if (cat && cat.color) {
|
||||
if (tds.className) tds.className += ' '; else tds.className = '';
|
||||
tds.className += cat.color;
|
||||
}
|
||||
|
||||
return tds;
|
||||
} );
|
||||
|
||||
if (resp.rows && resp.rows.length && !num_visible_items) {
|
||||
html += '<tr><td colspan="'+cols.length+'" align="center" style="padding-top:10px; padding-bottom:10px; font-weight:bold;">';
|
||||
html += 'All items were deleted on this page.';
|
||||
html += '</td></tr>';
|
||||
}
|
||||
|
||||
this.div.find('#d_history_table').html( html );
|
||||
},
|
||||
|
||||
jump_to_event_history: function() {
|
||||
// make a selection from the event filter menu
|
||||
var id = $('#fe_hist_event').val();
|
||||
if (id) Nav.go( '#History?sub=event_history&id=' + id );
|
||||
},
|
||||
|
||||
gosub_event_stats: function(args) {
|
||||
// request event stats
|
||||
if (!args.offset) args.offset = 0;
|
||||
if (!args.limit) args.limit = 50;
|
||||
app.api.post( 'app/get_event_history', copy_object(args), this.receive_event_stats.bind(this) );
|
||||
},
|
||||
|
||||
receive_event_stats: function(resp) {
|
||||
// render event stats page
|
||||
this.lastEventStatsResp = resp;
|
||||
|
||||
var html = '';
|
||||
var args = this.args;
|
||||
var rows = this.rows = resp.rows;
|
||||
|
||||
var size = get_inner_window_size();
|
||||
var col_width = Math.floor( ((size.width * 0.9) - 300) / 4 );
|
||||
|
||||
var event = find_object( app.schedule, { id: args.id } ) || null;
|
||||
if (!event) return app.doError("Could not locate event in schedule: " + args.id);
|
||||
|
||||
var cat = event.category ? find_object( app.categories, { id: event.category } ) : null;
|
||||
var group = event.target ? find_object( app.server_groups, { id: event.target } ) : null;
|
||||
var plugin = event.plugin ? find_object( app.plugins, { id: event.plugin } ) : null;
|
||||
|
||||
if (group && event.multiplex) {
|
||||
group = copy_object(group);
|
||||
group.multiplex = 1;
|
||||
}
|
||||
|
||||
app.setWindowTitle( "Event Stats: " + event.title );
|
||||
this.div.removeClass('loading');
|
||||
|
||||
html += this.getSidebarTabs( 'event_stats',
|
||||
[
|
||||
['history', "All Completed"],
|
||||
['event_stats', "Event Stats"],
|
||||
['event_history&id=' + args.id, "Event History"]
|
||||
]
|
||||
);
|
||||
html += '<div style="padding:20px 20px 30px 20px">';
|
||||
|
||||
html += '<fieldset style="margin-top:0px; margin-right:0px; padding-top:10px;"><legend>Event Stats</legend>';
|
||||
|
||||
html += '<div style="float:left; width:25%;">';
|
||||
html += '<div class="info_label">EVENT NAME</div>';
|
||||
html += '<div class="info_value"><a href="#Schedule?sub=edit_event&id='+event.id+'">' + this.getNiceEvent(event.title, col_width) + '</a></div>';
|
||||
|
||||
html += '<div class="info_label">CATEGORY NAME</div>';
|
||||
html += '<div class="info_value">' + this.getNiceCategory(cat, col_width) + '</div>';
|
||||
|
||||
html += '<div class="info_label">EVENT TIMING</div>';
|
||||
html += '<div class="info_value">' + (event.enabled ? summarize_event_timing(event.timing, event.timezone) : '(Disabled)') + '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div style="float:left; width:25%;">';
|
||||
html += '<div class="info_label">USERNAME</div>';
|
||||
html += '<div class="info_value">' + this.getNiceUsername(event, false, col_width) + '</div>';
|
||||
|
||||
html += '<div class="info_label">PLUGIN NAME</div>';
|
||||
html += '<div class="info_value">' + this.getNicePlugin(plugin, col_width) + '</div>';
|
||||
|
||||
html += '<div class="info_label">EVENT TARGET</div>';
|
||||
html += '<div class="info_value">' + this.getNiceGroup(group, event.target, col_width) + '</div>';
|
||||
html += '</div>';
|
||||
|
||||
var total_elapsed = 0;
|
||||
var total_cpu = 0;
|
||||
var total_mem = 0;
|
||||
var total_success = 0;
|
||||
var total_log_size = 0;
|
||||
var count = 0;
|
||||
|
||||
for (var idx = 0, len = rows.length; idx < len; idx++) {
|
||||
var job = rows[idx];
|
||||
if (job.action != 'job_complete') continue;
|
||||
|
||||
count++;
|
||||
total_elapsed += (job.elapsed || 0);
|
||||
if (job.cpu && job.cpu.total) total_cpu += (job.cpu.total / (job.cpu.count || 1));
|
||||
if (job.mem && job.mem.total) total_mem += (job.mem.total / (job.mem.count || 1));
|
||||
if (job.code == 0) total_success++;
|
||||
total_log_size += (job.log_file_size || 0);
|
||||
}
|
||||
if (!count) count = 1;
|
||||
|
||||
var nice_last_result = 'n/a';
|
||||
if (rows.length > 0) {
|
||||
var job = find_object( rows, { action: 'job_complete' } );
|
||||
if (job) nice_last_result = (job.code == 0) ? '<span class="color_label green"><i class="fa fa-check"> </i>Success</span>' : '<span class="color_label red"><i class="fa fa-warning"> </i>Error</span>';
|
||||
}
|
||||
|
||||
html += '<div style="float:left; width:25%;">';
|
||||
html += '<div class="info_label">AVG. ELAPSED</div>';
|
||||
html += '<div class="info_value">' + get_text_from_seconds(total_elapsed / count, true, false) + '</div>';
|
||||
|
||||
html += '<div class="info_label">AVG. CPU</div>';
|
||||
html += '<div class="info_value">' + short_float(total_cpu / count) + '%</div>';
|
||||
|
||||
html += '<div class="info_label">AVG. MEMORY</div>';
|
||||
html += '<div class="info_value">' + get_text_from_bytes( total_mem / count ) + '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div style="float:left; width:25%;">';
|
||||
html += '<div class="info_label">SUCCESS RATE</div>';
|
||||
html += '<div class="info_value">' + pct(total_success, count) + '</div>';
|
||||
|
||||
html += '<div class="info_label">LAST RESULT</div>';
|
||||
html += '<div class="info_value" style="position:relative; top:1px;">' + nice_last_result + '</div>';
|
||||
|
||||
html += '<div class="info_label">AVG. LOG SIZE</div>';
|
||||
html += '<div class="info_value">' + get_text_from_bytes( total_log_size / count ) + '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="clear"></div>';
|
||||
html += '</fieldset>';
|
||||
|
||||
// graph containers
|
||||
html += '<div style="margin-top:15px;">';
|
||||
html += '<div class="graph-title">Performance History</div>';
|
||||
html += '<div id="d_graph_hist_perf" style="position:relative; width:100%; height:300px; overflow:hidden;"><canvas id="c_graph_hist_perf"></canvas></div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div style="margin-top:10px; margin-bottom:20px; height:1px; background:#ddd;"></div>';
|
||||
|
||||
// cpu / mem graphs
|
||||
html += '<div style="margin-top:0px;">';
|
||||
html += '<div style="float:left; width:50%;">';
|
||||
html += '<div class="graph-title">CPU Usage History</div>';
|
||||
html += '<div id="d_graph_hist_cpu" style="position:relative; width:100%; margin-right:5px; height:225px; overflow:hidden;"><canvas id="c_graph_hist_cpu"></canvas></div>';
|
||||
html += '</div>';
|
||||
html += '<div style="float:left; width:50%;">';
|
||||
html += '<div class="graph-title">Memory Usage History</div>';
|
||||
html += '<div id="d_graph_hist_mem" style="position:relative; width:100%; margin-left:5px; height:225px; overflow:hidden;"><canvas id="c_graph_hist_mem"></canvas></div>';
|
||||
html += '</div>';
|
||||
html += '<div class="clear"></div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '</div>'; // padding
|
||||
html += '</div>'; // sidebar tabs
|
||||
|
||||
this.div.html( html );
|
||||
|
||||
// graphs
|
||||
this.render_perf_line_chart();
|
||||
this.render_cpu_line_chart();
|
||||
this.render_mem_line_chart();
|
||||
},
|
||||
|
||||
render_perf_line_chart: function() {
|
||||
// event perf over time
|
||||
var rows = this.rows;
|
||||
|
||||
var perf_keys = {};
|
||||
var perf_data = [];
|
||||
var perf_times = [];
|
||||
|
||||
// build perf data for chart
|
||||
// read backwards as server data is unshifted (descending by date, newest first)
|
||||
for (var idx = rows.length - 1; idx >= 0; idx--) {
|
||||
var job = rows[idx];
|
||||
if (job.action != 'job_complete') continue;
|
||||
|
||||
if (!job.perf) job.perf = { total: job.elapsed };
|
||||
if (!isa_hash(job.perf)) job.perf = parse_query_string( job.perf.replace(/\;/g, '&') );
|
||||
|
||||
var pscale = 1;
|
||||
if (job.perf.scale) {
|
||||
pscale = job.perf.scale;
|
||||
}
|
||||
|
||||
var perf = deep_copy_object( job.perf.perf ? job.perf.perf : job.perf );
|
||||
delete perf.scale;
|
||||
|
||||
// remove counters from pie
|
||||
for (var key in perf) {
|
||||
if (key.match(/^c_/)) delete perf[key];
|
||||
}
|
||||
|
||||
if (perf.t) { perf.total = perf.t; delete perf.t; }
|
||||
if ((num_keys(perf) > 1) && perf.total) {
|
||||
if (!perf.other) {
|
||||
var totes = 0;
|
||||
for (var key in perf) {
|
||||
if (key != 'total') totes += perf[key];
|
||||
}
|
||||
if (totes < perf.total) {
|
||||
perf.other = perf.total - totes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// divide everything by scale, so we get seconds
|
||||
for (var key in perf) {
|
||||
perf[key] /= pscale;
|
||||
}
|
||||
|
||||
perf_data.push( perf );
|
||||
for (var key in perf) {
|
||||
perf_keys[key] = 1;
|
||||
}
|
||||
|
||||
// track times as well
|
||||
perf_times.push( job.time_end || (job.time_start + job.elapsed) );
|
||||
} // foreach row
|
||||
|
||||
// build up timestamp data
|
||||
var tstamp_col = [];
|
||||
for (var idy = 0, ley = perf_times.length; idy < ley; idy++) {
|
||||
tstamp_col.push( perf_times[idy] * 1000 );
|
||||
} // foreach row
|
||||
|
||||
var sorted_keys = hash_keys_to_array(perf_keys).sort();
|
||||
var datasets = [];
|
||||
|
||||
for (var idx = 0, len = sorted_keys.length; idx < len; idx++) {
|
||||
var perf_key = sorted_keys[idx];
|
||||
var clr = 'rgb(' + this.graph_colors[ idx % this.graph_colors.length ] + ')';
|
||||
var dataset = {
|
||||
label: perf_key,
|
||||
backgroundColor: clr,
|
||||
borderColor: clr,
|
||||
fill: false,
|
||||
data: []
|
||||
};
|
||||
|
||||
for (var idy = 0, ley = perf_data.length; idy < ley; idy++) {
|
||||
var perf = perf_data[idy];
|
||||
var value = Math.max( 0, perf[perf_key] || 0 );
|
||||
dataset.data.push({ x: tstamp_col[idy], y: short_float(value) });
|
||||
} // foreach row
|
||||
|
||||
datasets.push( dataset );
|
||||
} // foreach key
|
||||
|
||||
this.charts.perf = new Chart( $('#c_graph_hist_perf').get(0).getContext('2d'), {
|
||||
type: 'line',
|
||||
data: { datasets: datasets },
|
||||
options: {
|
||||
animation: {
|
||||
duration: 0
|
||||
},
|
||||
responsive: true,
|
||||
responsiveAnimationDuration: 0,
|
||||
maintainAspectRatio: false,
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
fontStyle: 'bold',
|
||||
padding: 15
|
||||
}
|
||||
},
|
||||
title:{
|
||||
display: false,
|
||||
text: ""
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: "time",
|
||||
display: true,
|
||||
time: {
|
||||
parser: 'MM/DD/YYYY HH:mm',
|
||||
round: 'minute',
|
||||
tooltipFormat: 'll hh:mm a'
|
||||
},
|
||||
scaleLabel: {
|
||||
display: false,
|
||||
labelString: 'Date'
|
||||
}
|
||||
}, ],
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
callback: function(value, index, values) {
|
||||
if (value < 0) return '';
|
||||
return '' + get_text_from_seconds_round_custom(value, true);
|
||||
}
|
||||
},
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
// labelString: 'value'
|
||||
}
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
mode: 'nearest',
|
||||
intersect: false,
|
||||
callbacks: {
|
||||
label: function(tooltip, data) {
|
||||
var value = short_float(tooltip.yLabel);
|
||||
if (value >= 60) value = get_text_from_seconds( Math.floor(value), 1, 0 ).replace(/ \;/ig, ' ');
|
||||
else value = '' + value + " sec";
|
||||
return " " + datasets[tooltip.datasetIndex].label + ": " + value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
render_cpu_line_chart: function() {
|
||||
// event cpu usage over time
|
||||
var rows = this.rows;
|
||||
var color = Chart.helpers.color;
|
||||
|
||||
var col_avg = [];
|
||||
var col_max = [];
|
||||
|
||||
// build data for chart
|
||||
// read backwards as server data is unshifted (descending by date, newest first)
|
||||
for (var idx = rows.length - 1; idx >= 0; idx--) {
|
||||
var job = rows[idx];
|
||||
if (job.action != 'job_complete') continue;
|
||||
|
||||
if (!job.cpu) job.cpu = {};
|
||||
var x = (job.time_end || (job.time_start + job.elapsed)) * 1000;
|
||||
|
||||
col_avg.push({
|
||||
x: x,
|
||||
y: short_float( (job.cpu.total || 0) / (job.cpu.count || 1) )
|
||||
});
|
||||
|
||||
col_max.push({
|
||||
x: x,
|
||||
y: short_float( job.cpu.max || 0 )
|
||||
});
|
||||
} // foreach row
|
||||
|
||||
var datasets = [
|
||||
{
|
||||
label: "CPU Peak",
|
||||
borderColor: '#888888',
|
||||
fill: false,
|
||||
data: col_max
|
||||
},
|
||||
{
|
||||
label: "CPU Avg",
|
||||
borderColor: '#3f7ed5',
|
||||
backgroundColor: color('#3f7ed5').alpha(0.5).rgbString(),
|
||||
data: col_avg
|
||||
}
|
||||
];
|
||||
|
||||
this.charts.cpu = new Chart( $('#c_graph_hist_cpu').get(0).getContext('2d'), {
|
||||
type: 'line',
|
||||
data: { datasets: datasets },
|
||||
options: {
|
||||
animation: {
|
||||
duration: 0
|
||||
},
|
||||
responsive: true,
|
||||
responsiveAnimationDuration: 0,
|
||||
maintainAspectRatio: false,
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
fontStyle: 'bold',
|
||||
padding: 15
|
||||
}
|
||||
},
|
||||
title:{
|
||||
display: false,
|
||||
text: ""
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: "time",
|
||||
display: true,
|
||||
time: {
|
||||
parser: 'MM/DD/YYYY HH:mm',
|
||||
round: 'minute',
|
||||
tooltipFormat: 'll hh:mm a'
|
||||
},
|
||||
scaleLabel: {
|
||||
display: false,
|
||||
labelString: 'Date'
|
||||
}
|
||||
}, ],
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
callback: function(value, index, values) {
|
||||
return '' + Math.round(value) + '%';
|
||||
}
|
||||
},
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
// labelString: 'value'
|
||||
}
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
callbacks: {
|
||||
label: function(tooltip, data) {
|
||||
return " " + datasets[tooltip.datasetIndex].label + ": " + short_float(tooltip.yLabel) + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
render_mem_line_chart: function() {
|
||||
// event mem usage over time
|
||||
var rows = this.rows;
|
||||
var color = Chart.helpers.color;
|
||||
|
||||
var col_avg = [];
|
||||
var col_max = [];
|
||||
|
||||
// build data for chart
|
||||
// read backwards as server data is unshifted (descending by date, newest first)
|
||||
for (var idx = rows.length - 1; idx >= 0; idx--) {
|
||||
var job = rows[idx];
|
||||
if (job.action != 'job_complete') continue;
|
||||
|
||||
if (!job.mem) job.mem = {};
|
||||
var x = (job.time_end || (job.time_start + job.elapsed)) * 1000;
|
||||
|
||||
col_avg.push({
|
||||
x: x,
|
||||
y: short_float( (job.mem.total || 0) / (job.mem.count || 1) )
|
||||
});
|
||||
|
||||
col_max.push({
|
||||
x: x,
|
||||
y: short_float( job.mem.max || 0 )
|
||||
});
|
||||
} // foreach row
|
||||
|
||||
var datasets = [
|
||||
{
|
||||
label: "Mem Peak",
|
||||
borderColor: '#888888',
|
||||
fill: false,
|
||||
data: col_max
|
||||
},
|
||||
{
|
||||
label: "Mem Avg",
|
||||
borderColor: '#279321',
|
||||
backgroundColor: color('#279321').alpha(0.5).rgbString(),
|
||||
data: col_avg
|
||||
}
|
||||
];
|
||||
|
||||
this.charts.mem = new Chart( $('#c_graph_hist_mem').get(0).getContext('2d'), {
|
||||
type: 'line',
|
||||
data: { datasets: datasets },
|
||||
options: {
|
||||
animation: {
|
||||
duration: 0
|
||||
},
|
||||
responsive: true,
|
||||
responsiveAnimationDuration: 0,
|
||||
maintainAspectRatio: false,
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
fontStyle: 'bold',
|
||||
padding: 15
|
||||
}
|
||||
},
|
||||
title:{
|
||||
display: false,
|
||||
text: ""
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: "time",
|
||||
display: true,
|
||||
time: {
|
||||
parser: 'MM/DD/YYYY HH:mm',
|
||||
round: 'minute',
|
||||
tooltipFormat: 'll hh:mm a'
|
||||
},
|
||||
scaleLabel: {
|
||||
display: false,
|
||||
labelString: 'Date'
|
||||
}
|
||||
}, ],
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
callback: function(value, index, values) {
|
||||
return '' + get_text_from_bytes(value, 1);
|
||||
}
|
||||
},
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
// labelString: 'value'
|
||||
}
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
callbacks: {
|
||||
label: function(tooltip, data) {
|
||||
return " " + datasets[tooltip.datasetIndex].label + ": " + get_text_from_bytes(tooltip.yLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
gosub_event_history: function(args) {
|
||||
// show table of all history for a single event
|
||||
if (!args.offset) args.offset = 0;
|
||||
if (!args.limit) args.limit = 25;
|
||||
app.api.post( 'app/get_event_history', copy_object(args), this.receive_event_history.bind(this) );
|
||||
},
|
||||
|
||||
receive_event_history: function(resp) {
|
||||
// render event history
|
||||
this.lastEventHistoryResp = resp;
|
||||
|
||||
var html = '';
|
||||
var args = this.args;
|
||||
var rows = this.rows = resp.rows;
|
||||
|
||||
var size = get_inner_window_size();
|
||||
var col_width = Math.floor( ((size.width * 0.9) - 300) / 7 );
|
||||
|
||||
var event = find_object( app.schedule, { id: args.id } ) || null;
|
||||
if (!event) return app.doError("Could not locate event in schedule: " + args.id);
|
||||
|
||||
app.setWindowTitle( "Event History: " + event.title );
|
||||
this.div.removeClass('loading');
|
||||
|
||||
html += this.getSidebarTabs( 'event_history',
|
||||
[
|
||||
['history', "All Completed"],
|
||||
['event_stats&id=' + args.id, "Event Stats"],
|
||||
['event_history', "Event History"]
|
||||
]
|
||||
);
|
||||
html += '<div style="padding:20px 20px 30px 20px">';
|
||||
|
||||
var cols = ['Job ID', 'Hostname', 'Result', 'Start Date/Time', 'Elapsed Time', 'Avg CPU', 'Avg Mem'];
|
||||
|
||||
html += '<div class="subtitle">';
|
||||
html += 'Event History: ' + event.title;
|
||||
html += '<div class="clear"></div>';
|
||||
html += '</div>';
|
||||
|
||||
var self = this;
|
||||
var num_visible_items = 0;
|
||||
|
||||
html += this.getPaginatedTable( resp, cols, 'event', function(job, idx) {
|
||||
if (job.action != 'job_complete') return null;
|
||||
num_visible_items++;
|
||||
|
||||
var cpu_avg = 0;
|
||||
var mem_avg = 0;
|
||||
if (job.cpu) cpu_avg = short_float( (job.cpu.total || 0) / (job.cpu.count || 1) );
|
||||
if (job.mem) mem_avg = short_float( (job.mem.total || 0) / (job.mem.count || 1) );
|
||||
|
||||
var tds = [
|
||||
'<div class="td_big" style="white-space:nowrap;"><a href="#JobDetails?id='+job.id+'"><i class="fa fa-pie-chart"> </i><b>' + job.id.substring(0, 11) + '</b></span></div>',
|
||||
self.getNiceGroup( null, job.hostname, col_width ),
|
||||
(job.code == 0) ? '<span class="color_label green"><i class="fa fa-check"> </i>Success</span>' : '<span class="color_label red"><i class="fa fa-warning"> </i>Error</span>',
|
||||
get_nice_date_time( job.time_start, false, true ),
|
||||
get_text_from_seconds( job.elapsed, true, false ),
|
||||
'' + cpu_avg + '%',
|
||||
get_text_from_bytes(mem_avg)
|
||||
// actions.join(' | ')
|
||||
];
|
||||
|
||||
return tds;
|
||||
} );
|
||||
|
||||
if (resp.rows && resp.rows.length && !num_visible_items) {
|
||||
html += '<tr><td colspan="'+cols.length+'" align="center" style="padding-top:10px; padding-bottom:10px; font-weight:bold;">';
|
||||
html += 'All items were deleted on this page.';
|
||||
html += '</td></tr>';
|
||||
}
|
||||
|
||||
html += '</div>'; // padding
|
||||
html += '</div>'; // sidebar tabs
|
||||
|
||||
this.div.html( html );
|
||||
},
|
||||
|
||||
onStatusUpdate: function(data) {
|
||||
// received status update (websocket), update sub-page if needed
|
||||
if (data.jobs_changed && (this.args.sub == 'history')) this.get_history();
|
||||
},
|
||||
|
||||
onResizeDelay: function(size) {
|
||||
// called 250ms after latest window resize
|
||||
// so we can run more expensive redraw operations
|
||||
switch (this.args.sub) {
|
||||
case 'history':
|
||||
if (this.lastHistoryResp) {
|
||||
this.receive_history( this.lastHistoryResp );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'event_stats':
|
||||
if (this.lastEventStatsResp) {
|
||||
this.receive_event_stats( this.lastEventStatsResp );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'event_history':
|
||||
if (this.lastEventHistoryResp) {
|
||||
this.receive_event_history( this.lastEventHistoryResp );
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onDeactivate: function() {
|
||||
// called when page is deactivated
|
||||
for (var key in this.charts) {
|
||||
this.charts[key].destroy();
|
||||
}
|
||||
this.charts = {};
|
||||
|
||||
delete this.rows;
|
||||
if (this.args && (this.args.sub == 'event_stats')) this.div.html( '' );
|
||||
return true;
|
||||
}
|
||||
|
||||
});
|
634
workspaces/base-workspace/Cronicle-0.8.61/htdocs/js/pages/Home.class.js
Executable file
|
@ -0,0 +1,634 @@
|
|||
Class.subclass( Page.Base, "Page.Home", {
|
||||
|
||||
bar_width: 100,
|
||||
|
||||
onInit: function() {
|
||||
// called once at page load
|
||||
this.worker = new Worker('js/home-worker.js');
|
||||
this.worker.onmessage = this.render_upcoming_events.bind(this);
|
||||
|
||||
var html = '';
|
||||
html += '<div style="padding:10px 20px 20px 20px">';
|
||||
|
||||
// header stats
|
||||
html += '<div id="d_home_header_stats"></div>';
|
||||
html += '<div style="height:20px;"></div>';
|
||||
|
||||
// active jobs
|
||||
html += '<div class="subtitle">';
|
||||
html += 'Active Jobs';
|
||||
html += '<div class="clear"></div>';
|
||||
html += '</div>';
|
||||
html += '<div id="d_home_active_jobs"></div>';
|
||||
html += '<div style="height:20px;"></div>';
|
||||
|
||||
// queued jobs
|
||||
html += '<div id="d_home_queue_container" style="display:none">';
|
||||
html += '<div class="subtitle">';
|
||||
html += 'Event Queues';
|
||||
html += '<div class="clear"></div>';
|
||||
html += '</div>';
|
||||
html += '<div id="d_home_queued_jobs"></div>';
|
||||
html += '<div style="height:20px;"></div>';
|
||||
html += '</div>';
|
||||
|
||||
// upcoming events
|
||||
html += '<div id="d_home_upcoming_header" class="subtitle">';
|
||||
html += '</div>';
|
||||
html += '<div id="d_home_upcoming_events" class="loading"></div>';
|
||||
html += '</div>'; // container
|
||||
|
||||
this.div.html( html );
|
||||
},
|
||||
|
||||
onActivate: function(args) {
|
||||
// page activation
|
||||
if (!this.requireLogin(args)) return true;
|
||||
|
||||
if (!args) args = {};
|
||||
this.args = args;
|
||||
|
||||
app.setWindowTitle('Home');
|
||||
app.showTabBar(true);
|
||||
|
||||
this.upcoming_offset = 0;
|
||||
|
||||
// presort some stuff for the filter menus
|
||||
app.categories.sort( function(a, b) {
|
||||
// return (b.title < a.title) ? 1 : -1;
|
||||
return a.title.toLowerCase().localeCompare( b.title.toLowerCase() );
|
||||
} );
|
||||
app.plugins.sort( function(a, b) {
|
||||
// return (b.title < a.title) ? 1 : -1;
|
||||
return a.title.toLowerCase().localeCompare( b.title.toLowerCase() );
|
||||
} );
|
||||
|
||||
// render upcoming event filters
|
||||
var html = '';
|
||||
html += 'Upcoming Events';
|
||||
|
||||
html += '<div class="subtitle_widget"><i class="fa fa-search"> </i><input type="text" id="fe_home_keywords" size="10" placeholder="Find events..." style="border:0px;" value="' + escape_text_field_value( args.keywords ) + '"/></div>';
|
||||
|
||||
html += '<div class="subtitle_widget"><i class="fa fa-chevron-down"> </i><select id="fe_home_target" class="subtitle_menu" style="width:75px;" onChange="$P().set_search_filters()"><option value="">All Servers</option>' + this.render_target_menu_options( args.target ) + '</select></div>';
|
||||
html += '<div class="subtitle_widget"><i class="fa fa-chevron-down"> </i><select id="fe_home_plugin" class="subtitle_menu" style="width:75px;" onChange="$P().set_search_filters()"><option value="">All Plugins</option>' + render_menu_options( app.plugins, args.plugin, false ) + '</select></div>';
|
||||
html += '<div class="subtitle_widget"><i class="fa fa-chevron-down"> </i><select id="fe_home_cat" class="subtitle_menu" style="width:95px;" onChange="$P().set_search_filters()"><option value="">All Categories</option>' + render_menu_options( app.categories, args.category, false ) + '</select></div>';
|
||||
|
||||
html += '<div class="clear"></div>';
|
||||
|
||||
$('#d_home_upcoming_header').html( html );
|
||||
|
||||
setTimeout( function() {
|
||||
$('#fe_home_keywords').keypress( function(event) {
|
||||
if (event.keyCode == '13') { // enter key
|
||||
event.preventDefault();
|
||||
$P().set_search_filters();
|
||||
}
|
||||
} );
|
||||
}, 1 );
|
||||
|
||||
// refresh datas
|
||||
$('#d_home_active_jobs').html( this.get_active_jobs_html() );
|
||||
this.refresh_upcoming_events();
|
||||
this.refresh_header_stats();
|
||||
this.refresh_event_queues();
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
refresh_header_stats: function() {
|
||||
// refresh daemons stats in header fieldset
|
||||
var html = '';
|
||||
var stats = app.state ? (app.state.stats || {}) : {};
|
||||
var servers = app.servers || {};
|
||||
|
||||
html += '<fieldset style="margin-top:0px; margin-right:0px; padding-top:10px;"><legend>Server Stats</legend>';
|
||||
|
||||
html += '<div style="float:left; width:25%;">';
|
||||
var active_events = find_objects( app.schedule, { enabled: 1 } );
|
||||
|
||||
html += '<div class="info_label">TOTAL EVENTS</div>';
|
||||
html += '<div class="info_value">' + commify( active_events.length ) + '</div>';
|
||||
|
||||
html += '<div class="info_label">TOTAL CATEGORIES</div>';
|
||||
html += '<div class="info_value">' + commify( app.categories.length ) + '</div>';
|
||||
|
||||
html += '<div class="info_label">TOTAL PLUGINS</div>';
|
||||
html += '<div class="info_value">' + commify( app.plugins.length ) + '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div style="float:left; width:25%;">';
|
||||
html += '<div class="info_label">JOBS COMPLETED TODAY</div>';
|
||||
html += '<div class="info_value">' + commify( stats.jobs_completed || 0 ) + '</div>';
|
||||
|
||||
html += '<div class="info_label">JOBS FAILED TODAY</div>';
|
||||
html += '<div class="info_value">' + commify( stats.jobs_failed || 0 ) + '</div>';
|
||||
|
||||
html += '<div class="info_label">JOB SUCCESS RATE</div>';
|
||||
html += '<div class="info_value">' + pct( (stats.jobs_completed || 0) - (stats.jobs_failed || 0), stats.jobs_completed || 1 ) + '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div style="float:left; width:25%;">';
|
||||
html += '<div class="info_label">TOTAL SERVERS</div>';
|
||||
html += '<div class="info_value">' + commify( num_keys(servers) ) + '</div>';
|
||||
|
||||
var total_cpu = 0;
|
||||
var total_mem = 0;
|
||||
for (var hostname in servers) {
|
||||
// daemon process cpu, all servers
|
||||
var server = servers[hostname];
|
||||
if (server.data && !server.disabled) {
|
||||
total_cpu += (server.data.cpu || 0);
|
||||
total_mem += (server.data.mem || 0);
|
||||
}
|
||||
}
|
||||
for (var id in app.activeJobs) {
|
||||
// active job process cpu, all jobs
|
||||
var job = app.activeJobs[id];
|
||||
if (job.cpu) total_cpu += (job.cpu.current || 0);
|
||||
if (job.mem) total_mem += (job.mem.current || 0);
|
||||
}
|
||||
html += '<div class="info_label">TOTAL CPU IN USE</div>';
|
||||
html += '<div class="info_value">' + short_float(total_cpu) + '%</div>';
|
||||
|
||||
html += '<div class="info_label">TOTAL RAM IN USE</div>';
|
||||
html += '<div class="info_value">' + get_text_from_bytes(total_mem) + '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div style="float:left; width:25%;">';
|
||||
var mserver = servers[ app.masterHostname ] || {};
|
||||
html += '<div class="info_label">MASTER SERVER UPTIME</div>';
|
||||
html += '<div class="info_value">' + get_text_from_seconds( mserver.uptime || 0, false, true ) + '</div>';
|
||||
|
||||
var job_avg = (stats.jobs_elapsed || 0) / (stats.jobs_completed || 1);
|
||||
html += '<div class="info_label">AVERAGE JOB DURATION</div>';
|
||||
html += '<div class="info_value">' + get_text_from_seconds( job_avg, false, true ) + '</div>';
|
||||
|
||||
var log_size_avg = (stats.jobs_log_size || 0) / (stats.jobs_completed || 1);
|
||||
html += '<div class="info_label">AVERAGE JOB LOG SIZE</div>';
|
||||
html += '<div class="info_value">' + get_text_from_bytes(log_size_avg) + '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="clear"></div>';
|
||||
html += '</fieldset>';
|
||||
|
||||
$('#d_home_header_stats').html( html );
|
||||
},
|
||||
|
||||
refresh_upcoming_events: function() {
|
||||
// send message to worker to refresh upcoming
|
||||
this.worker_start_time = hires_time_now();
|
||||
this.worker.postMessage({
|
||||
default_tz: app.tz,
|
||||
schedule: app.schedule,
|
||||
state: app.state,
|
||||
categories: app.categories,
|
||||
plugins: app.plugins
|
||||
});
|
||||
},
|
||||
|
||||
nav_upcoming: function(offset) {
|
||||
// refresh upcoming events with new offset
|
||||
this.upcoming_offset = offset;
|
||||
this.render_upcoming_events({
|
||||
data: this.upcoming_events
|
||||
});
|
||||
},
|
||||
|
||||
set_search_filters: function() {
|
||||
// grab values from search filters, and refresh
|
||||
var args = this.args;
|
||||
|
||||
args.plugin = $('#fe_home_plugin').val();
|
||||
if (!args.plugin) delete args.plugin;
|
||||
|
||||
args.target = $('#fe_home_target').val();
|
||||
if (!args.target) delete args.target;
|
||||
|
||||
args.category = $('#fe_home_cat').val();
|
||||
if (!args.category) delete args.category;
|
||||
|
||||
args.keywords = $('#fe_home_keywords').val();
|
||||
if (!args.keywords) delete args.keywords;
|
||||
|
||||
this.nav_upcoming(0);
|
||||
},
|
||||
|
||||
render_upcoming_events: function(e) {
|
||||
// receive data from worker, render table now
|
||||
var self = this;
|
||||
var html = '';
|
||||
var now = app.epoch || hires_time_now();
|
||||
var args = this.args;
|
||||
this.upcoming_events = e.data;
|
||||
|
||||
/*var elapsed = now - this.worker_start_time;
|
||||
delete this.worker_start_time;
|
||||
Debug.trace('Home', "Worker elapsed time: " + elapsed + ' sec for ' + app.schedule.length + ' events.');*/
|
||||
|
||||
// apply filters
|
||||
var events = [];
|
||||
|
||||
for (var idx = 0, len = e.data.length; idx < len; idx++) {
|
||||
var stub = e.data[idx];
|
||||
var item = find_object( app.schedule, { id: stub.id } ) || {};
|
||||
|
||||
// category filter
|
||||
if (args.category && (item.category != args.category)) continue;
|
||||
|
||||
// plugin filter
|
||||
if (args.plugin && (item.plugin != args.plugin)) continue;
|
||||
|
||||
// server group filter
|
||||
if (args.target && (item.target != args.target)) continue;
|
||||
|
||||
// keyword filter
|
||||
var words = [item.title, item.username, item.notes, item.target].join(' ').toLowerCase();
|
||||
if (args.keywords && words.indexOf(args.keywords.toLowerCase()) == -1) continue;
|
||||
|
||||
events.push( stub );
|
||||
} // foreach item in schedule
|
||||
|
||||
var size = get_inner_window_size();
|
||||
var col_width = Math.floor( ((size.width * 0.9) + 50) / 7 );
|
||||
|
||||
var cols = ['Event Name', 'Category', 'Plugin', 'Target', 'Scheduled Time', 'Countdown', 'Actions'];
|
||||
var limit = 25;
|
||||
|
||||
html += this.getPaginatedTable({
|
||||
resp: {
|
||||
rows: events.slice(this.upcoming_offset, this.upcoming_offset + limit),
|
||||
list: {
|
||||
length: events.length
|
||||
}
|
||||
},
|
||||
cols: cols,
|
||||
data_type: 'pending event',
|
||||
limit: limit,
|
||||
offset: this.upcoming_offset,
|
||||
pagination_link: '$P().nav_upcoming',
|
||||
|
||||
callback: function(stub, idx) {
|
||||
var item = find_object( app.schedule, { id: stub.id } ) || {};
|
||||
// var dargs = get_date_args( stub.epoch );
|
||||
var margs = moment.tz(stub.epoch * 1000, item.timezone || app.tz);
|
||||
|
||||
var actions = [
|
||||
'<a href="#Schedule?sub=edit_event&id='+item.id+'"><b>Edit Event</b></a>'
|
||||
];
|
||||
|
||||
var cat = item.category ? find_object( app.categories, { id: item.category } ) : null;
|
||||
var group = item.target ? find_object( app.server_groups, { id: item.target } ) : null;
|
||||
var plugin = item.plugin ? find_object( app.plugins, { id: item.plugin } ) : null;
|
||||
|
||||
var nice_countdown = 'Now';
|
||||
if (stub.epoch > now) {
|
||||
nice_countdown = get_text_from_seconds_round( Math.max(60, stub.epoch - now), false );
|
||||
}
|
||||
|
||||
if (group && item.multiplex) {
|
||||
group = copy_object(group);
|
||||
group.multiplex = 1;
|
||||
}
|
||||
|
||||
var tds = [
|
||||
'<div class="td_big" style="white-space:nowrap;"><a href="#Schedule?sub=edit_event&id='+item.id+'">' + self.getNiceEvent('<b>' + item.title + '</b>', col_width) + '</a></div>',
|
||||
self.getNiceCategory( cat, col_width ),
|
||||
self.getNicePlugin( plugin, col_width ),
|
||||
self.getNiceGroup( group, item.target, col_width ),
|
||||
// dargs.hour12 + ':' + dargs.mi + ' ' + dargs.ampm.toUpperCase(),
|
||||
margs.format("h:mm A z"),
|
||||
nice_countdown,
|
||||
actions.join(' | ')
|
||||
];
|
||||
|
||||
if (cat && cat.color) {
|
||||
if (tds.className) tds.className += ' '; else tds.className = '';
|
||||
tds.className += cat.color;
|
||||
}
|
||||
|
||||
return tds;
|
||||
} // row callback
|
||||
}); // table
|
||||
|
||||
$('#d_home_upcoming_events').removeClass('loading').html( html );
|
||||
},
|
||||
|
||||
get_active_jobs_html: function() {
|
||||
// get html for active jobs table
|
||||
var html = '';
|
||||
|
||||
var size = get_inner_window_size();
|
||||
var col_width = Math.floor( ((size.width * 0.9) + 50) / 8 );
|
||||
|
||||
// copy jobs to array
|
||||
var jobs = [];
|
||||
for (var id in app.activeJobs) {
|
||||
jobs.push( app.activeJobs[id] );
|
||||
}
|
||||
|
||||
// sort events by time_start descending
|
||||
this.jobs = jobs.sort( function(a, b) {
|
||||
return (a.time_start < b.time_start) ? 1 : -1;
|
||||
} );
|
||||
|
||||
var cols = ['Job ID', 'Event Name', 'Category', 'Hostname', 'Elapsed', 'Progress', 'Remaining', 'Actions'];
|
||||
|
||||
// render table
|
||||
var self = this;
|
||||
html += this.getBasicTable( this.jobs, cols, 'active job', function(job, idx) {
|
||||
var actions = [
|
||||
// '<span class="link" onMouseUp="$P().go_job_details('+idx+')"><b>Details</b></span>',
|
||||
'<span class="link" onMouseUp="$P().abort_job('+idx+')"><b>Abort Job</b></span>'
|
||||
];
|
||||
|
||||
var cat = job.category ? find_object( app.categories || [], { id: job.category } ) : { title: 'n/a' };
|
||||
// var group = item.target ? find_object( app.server_groups || [], { id: item.target } ) : null;
|
||||
var plugin = job.plugin ? find_object( app.plugins || [], { id: job.plugin } ) : { title: 'n/a' };
|
||||
var tds = null;
|
||||
|
||||
if (job.pending && job.log_file) {
|
||||
// job in retry delay
|
||||
tds = [
|
||||
'<div class="td_big"><span class="link" onMouseUp="$P().go_job_details('+idx+')">' + self.getNiceJob(job.id) + '</span></div>',
|
||||
self.getNiceEvent( job.event_title, col_width ),
|
||||
self.getNiceCategory( cat, col_width ),
|
||||
// self.getNicePlugin( plugin ),
|
||||
self.getNiceGroup( null, job.hostname, col_width ),
|
||||
'<div id="d_home_jt_elapsed_'+job.id+'">' + self.getNiceJobElapsedTime(job) + '</div>',
|
||||
'<div id="d_home_jt_progress_'+job.id+'">' + self.getNiceJobPendingText(job) + '</div>',
|
||||
'n/a',
|
||||
actions.join(' | ')
|
||||
];
|
||||
}
|
||||
else if (job.pending) {
|
||||
// multiplex stagger delay
|
||||
tds = [
|
||||
'<div class="td_big">' + self.getNiceJob(job.id) + '</div>',
|
||||
self.getNiceEvent( job.event_title, col_width ),
|
||||
self.getNiceCategory( cat, col_width ),
|
||||
// self.getNicePlugin( plugin ),
|
||||
self.getNiceGroup( null, job.hostname, col_width ),
|
||||
'n/a',
|
||||
'<div id="d_home_jt_progress_'+job.id+'">' + self.getNiceJobPendingText(job) + '</div>',
|
||||
'n/a',
|
||||
actions.join(' | ')
|
||||
];
|
||||
} // pending job
|
||||
else {
|
||||
// active job
|
||||
tds = [
|
||||
'<div class="td_big"><span class="link" onMouseUp="$P().go_job_details('+idx+')">' + self.getNiceJob(job.id) + '</span></div>',
|
||||
self.getNiceEvent( job.event_title, col_width ),
|
||||
self.getNiceCategory( cat, col_width ),
|
||||
// self.getNicePlugin( plugin ),
|
||||
self.getNiceGroup( null, job.hostname, col_width ),
|
||||
'<div id="d_home_jt_elapsed_'+job.id+'">' + self.getNiceJobElapsedTime(job) + '</div>',
|
||||
'<div id="d_home_jt_progress_'+job.id+'">' + self.getNiceJobProgressBar(job) + '</div>',
|
||||
'<div id="d_home_jt_remaining_'+job.id+'">' + self.getNiceJobRemainingTime(job) + '</div>',
|
||||
actions.join(' | ')
|
||||
];
|
||||
} // active job
|
||||
|
||||
if (cat && cat.color) {
|
||||
if (tds.className) tds.className += ' '; else tds.className = '';
|
||||
tds.className += cat.color;
|
||||
}
|
||||
|
||||
return tds;
|
||||
} );
|
||||
|
||||
return html;
|
||||
},
|
||||
|
||||
refresh_event_queues: function() {
|
||||
// update display of event queues, if any
|
||||
var self = this;
|
||||
var total_count = 0;
|
||||
for (var key in app.eventQueue) {
|
||||
total_count += app.eventQueue[key] || 0;
|
||||
}
|
||||
|
||||
if (!total_count) {
|
||||
$('#d_home_queue_container').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
var size = get_inner_window_size();
|
||||
var col_width = Math.floor( ((size.width * 0.9) + 50) / 6 );
|
||||
var cols = ['Event Name', 'Category', 'Plugin', 'Target', 'Queued Jobs', 'Actions'];
|
||||
|
||||
var stubs = [];
|
||||
var sorted_ids = hash_keys_to_array(app.eventQueue).sort( function(a, b) {
|
||||
return (app.eventQueue[a] < app.eventQueue[b]) ? 1 : -1;
|
||||
} );
|
||||
sorted_ids.forEach( function(id) {
|
||||
if (app.eventQueue[id]) stubs.push({ id: id });
|
||||
} );
|
||||
|
||||
this.queue_stubs = stubs;
|
||||
|
||||
// render table
|
||||
var html = '';
|
||||
html += this.getBasicTable( stubs, cols, 'event', function(stub, idx) {
|
||||
var queue_count = app.eventQueue[ stub.id ] || 0;
|
||||
var item = find_object( app.schedule, { id: stub.id } ) || {};
|
||||
|
||||
// for flush dialog
|
||||
stub.title = item.title;
|
||||
|
||||
var cat = item.category ? find_object( app.categories, { id: item.category } ) : null;
|
||||
var group = item.target ? find_object( app.server_groups, { id: item.target } ) : null;
|
||||
var plugin = item.plugin ? find_object( app.plugins, { id: item.plugin } ) : null;
|
||||
|
||||
var actions = [
|
||||
'<span class="link" onMouseUp="$P().flush_event_queue('+idx+')"><b>Flush Queue</b></span>'
|
||||
];
|
||||
|
||||
var tds = [
|
||||
'<div class="td_big" style="white-space:nowrap;"><a href="#Schedule?sub=edit_event&id='+item.id+'">' + self.getNiceEvent('<b>' + item.title + '</b>', col_width) + '</a></div>',
|
||||
self.getNiceCategory( cat, col_width ),
|
||||
self.getNicePlugin( plugin, col_width ),
|
||||
self.getNiceGroup( group, item.target, col_width ),
|
||||
commify( queue_count ),
|
||||
actions.join(' | ')
|
||||
];
|
||||
|
||||
if (cat && cat.color) {
|
||||
if (tds.className) tds.className += ' '; else tds.className = '';
|
||||
tds.className += cat.color;
|
||||
}
|
||||
|
||||
return tds;
|
||||
|
||||
} ); // getBasicTable
|
||||
|
||||
$('#d_home_queued_jobs').html( html );
|
||||
$('#d_home_queue_container').show();
|
||||
},
|
||||
|
||||
go_job_details: function(idx) {
|
||||
// jump to job details page
|
||||
var job = this.jobs[idx];
|
||||
Nav.go( '#JobDetails?id=' + job.id );
|
||||
},
|
||||
|
||||
abort_job: function(idx) {
|
||||
// abort job, after confirmation
|
||||
var job = this.jobs[idx];
|
||||
|
||||
app.confirm( '<span style="color:red">Abort Job</span>', "Are you sure you want to abort the job “<b>"+job.id+"</b>”?</br>(Event: "+job.event_title+")", "Abort", function(result) {
|
||||
if (result) {
|
||||
app.showProgress( 1.0, "Aborting job..." );
|
||||
app.api.post( 'app/abort_job', job, function(resp) {
|
||||
app.hideProgress();
|
||||
app.showMessage('success', "Job '"+job.event_title+"' was aborted successfully.");
|
||||
} );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
flush_event_queue: function(idx) {
|
||||
// abort job, after confirmation
|
||||
var stub = this.queue_stubs[idx];
|
||||
|
||||
app.confirm( '<span style="color:red">Flush Event Queue</span>', "Are you sure you want to flush the queue for event “<b>"+stub.title+"</b>”?", "Flush", function(result) {
|
||||
if (result) {
|
||||
app.showProgress( 1.0, "Flushing event queue..." );
|
||||
app.api.post( 'app/flush_event_queue', stub, function(resp) {
|
||||
app.hideProgress();
|
||||
app.showMessage('success', "Event queue for '"+stub.title+"' was flushed successfully.");
|
||||
} );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
getNiceJobElapsedTime: function(job) {
|
||||
// render nice elapsed time display
|
||||
var elapsed = Math.floor( Math.max( 0, app.epoch - job.time_start ) );
|
||||
return get_text_from_seconds( elapsed, true, false );
|
||||
},
|
||||
|
||||
getNiceJobProgressBar: function(job) {
|
||||
// render nice progress bar for job
|
||||
var html = '';
|
||||
var counter = Math.min(1, Math.max(0, job.progress || 1));
|
||||
var cx = Math.floor( counter * this.bar_width );
|
||||
var extra_classes = '';
|
||||
var extra_attribs = '';
|
||||
if (counter == 1.0) extra_classes = 'indeterminate';
|
||||
else extra_attribs = 'title="'+Math.floor( (counter / 1.0) * 100 )+'%"';
|
||||
|
||||
html += '<div class="progress_bar_container '+extra_classes+'" style="width:'+this.bar_width+'px; margin:0;" '+extra_attribs+'>';
|
||||
html += '<div class="progress_bar_inner" style="width:'+cx+'px;"></div>';
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
},
|
||||
|
||||
getNiceJobRemainingTime: function(job) {
|
||||
// get nice job remaining time, using elapsed and progress
|
||||
var elapsed = Math.floor( Math.max( 0, app.epoch - job.time_start ) );
|
||||
var progress = job.progress || 0;
|
||||
if ((elapsed >= 10) && (progress > 0) && (progress < 1.0)) {
|
||||
var sec_remain = Math.floor(((1.0 - progress) * elapsed) / progress);
|
||||
return get_text_from_seconds( sec_remain, true, true );
|
||||
}
|
||||
else return 'n/a';
|
||||
},
|
||||
|
||||
getNiceJobPendingText: function(job) {
|
||||
// get nice display for pending job status
|
||||
var html = '';
|
||||
|
||||
// if job has a log_file, it's in a retry delay, otherwise it's pending (multiplex stagger)
|
||||
html += (job.log_file ? 'Retry' : 'Pending');
|
||||
|
||||
// countdown to actual launch
|
||||
var nice_countdown = get_text_from_seconds( Math.max(0, job.when - app.epoch), true, true );
|
||||
html += ' (' + nice_countdown + ')';
|
||||
|
||||
return html;
|
||||
},
|
||||
|
||||
onStatusUpdate: function(data) {
|
||||
// received status update (websocket), update page if needed
|
||||
if (data.jobs_changed) {
|
||||
// refresh tables
|
||||
$('#d_home_active_jobs').html( this.get_active_jobs_html() );
|
||||
}
|
||||
else {
|
||||
// update progress, time remaining, no refresh
|
||||
for (var id in app.activeJobs) {
|
||||
var job = app.activeJobs[id];
|
||||
|
||||
if (job.pending) {
|
||||
// update countdown
|
||||
$('#d_home_jt_progress_' + job.id).html( this.getNiceJobPendingText(job) );
|
||||
|
||||
if (job.log_file) {
|
||||
// retry delay
|
||||
$('#d_home_jt_elapsed_' + job.id).html( this.getNiceJobElapsedTime(job) );
|
||||
}
|
||||
} // pending job
|
||||
else {
|
||||
$('#d_home_jt_elapsed_' + job.id).html( this.getNiceJobElapsedTime(job) );
|
||||
$('#d_home_jt_remaining_' + job.id).html( this.getNiceJobRemainingTime(job) );
|
||||
|
||||
// update progress bar without redrawing it (so animation doesn't jitter)
|
||||
var counter = job.progress || 1;
|
||||
var cx = Math.floor( counter * this.bar_width );
|
||||
var prog_cont = $('#d_home_jt_progress_' + job.id + ' > div.progress_bar_container');
|
||||
|
||||
if ((counter == 1.0) && !prog_cont.hasClass('indeterminate')) {
|
||||
prog_cont.addClass('indeterminate').attr('title', "");
|
||||
}
|
||||
else if ((counter < 1.0) && prog_cont.hasClass('indeterminate')) {
|
||||
prog_cont.removeClass('indeterminate');
|
||||
}
|
||||
|
||||
if (counter < 1.0) prog_cont.attr('title', '' + Math.floor( (counter / 1.0) * 100 ) + '%');
|
||||
|
||||
prog_cont.find('> div.progress_bar_inner').css( 'width', '' + cx + 'px' );
|
||||
} // active job
|
||||
} // foreach job
|
||||
} // quick update
|
||||
},
|
||||
|
||||
onDataUpdate: function(key, value) {
|
||||
// recieved data update (websocket)
|
||||
switch (key) {
|
||||
case 'state':
|
||||
case 'schedule':
|
||||
// state update (new cursors)
|
||||
// $('#d_home_upcoming_events').html( this.get_upcoming_events_html() );
|
||||
this.refresh_upcoming_events();
|
||||
this.refresh_header_stats();
|
||||
break;
|
||||
|
||||
case 'eventQueue':
|
||||
this.refresh_event_queues();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onResizeDelay: function(size) {
|
||||
// called 250ms after latest window resize
|
||||
// so we can run more expensive redraw operations
|
||||
$('#d_home_active_jobs').html( this.get_active_jobs_html() );
|
||||
this.refresh_header_stats();
|
||||
this.refresh_event_queues();
|
||||
|
||||
if (this.upcoming_events) {
|
||||
this.render_upcoming_events({
|
||||
data: this.upcoming_events
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onDeactivate: function() {
|
||||
// called when page is deactivated
|
||||
// this.div.html( '' );
|
||||
return true;
|
||||
}
|
||||
|
||||
} );
|
1357
workspaces/base-workspace/Cronicle-0.8.61/htdocs/js/pages/JobDetails.class.js
Executable file
416
workspaces/base-workspace/Cronicle-0.8.61/htdocs/js/pages/Login.class.js
Executable file
|
@ -0,0 +1,416 @@
|
|||
Class.subclass( Page.Base, "Page.Login", {
|
||||
|
||||
onInit: function() {
|
||||
// called once at page load
|
||||
// var html = 'Now is the time (LOGIN)';
|
||||
// this.div.html( html );
|
||||
},
|
||||
|
||||
onActivate: function(args) {
|
||||
// page activation
|
||||
if (app.user) {
|
||||
// user already logged in
|
||||
setTimeout( function() { Nav.go(app.navAfterLogin || config.DefaultPage) }, 1 );
|
||||
return true;
|
||||
}
|
||||
else if (args.u && args.h) {
|
||||
this.showPasswordResetForm(args);
|
||||
return true;
|
||||
}
|
||||
else if (args.create) {
|
||||
this.showCreateAccountForm();
|
||||
return true;
|
||||
}
|
||||
else if (args.recover) {
|
||||
this.showRecoverPasswordForm();
|
||||
return true;
|
||||
}
|
||||
|
||||
app.setWindowTitle('Login');
|
||||
app.showTabBar(false);
|
||||
|
||||
this.div.css({ 'padding-top':'75px', 'padding-bottom':'75px' });
|
||||
var html = '';
|
||||
// html += '<iframe name="i_login" id="i_login" src="blank.html" width="1" height="1" style="display:none"></iframe>';
|
||||
// html += '<form id="f_login" method="post" action="/api/user/login?format=jshtml&callback=window.parent.%24P%28%29.doFrameLogin" target="i_login">';
|
||||
|
||||
html += '<div class="inline_dialog_container">';
|
||||
html += '<div class="dialog_title shade-light">User Login</div>';
|
||||
html += '<div class="dialog_content">';
|
||||
html += '<center><table style="margin:0px;">';
|
||||
html += '<tr>';
|
||||
html += '<td align="right" class="table_label">Username:</td>';
|
||||
html += '<td align="left" class="table_value"><div><input type="text" name="username" id="fe_login_username" size="30" spellcheck="false" value="'+(app.getPref('username') || '')+'"/></div></td>';
|
||||
html += '</tr>';
|
||||
html += '<tr><td colspan="2"><div class="table_spacer"></div></td></tr>';
|
||||
html += '<tr>';
|
||||
html += '<td align="right" class="table_label">Password:</td>';
|
||||
html += '<td align="left" class="table_value"><div><input type="' + app.get_password_type() + '" name="password" id="fe_login_password" size="30" spellcheck="false" value=""/>' + app.get_password_toggle_html() + '</div></td>';
|
||||
html += '</tr>';
|
||||
html += '<tr><td colspan="2"><div class="table_spacer"></div></td></tr>';
|
||||
html += '</table></center>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="dialog_buttons"><center><table><tr>';
|
||||
if (config.free_accounts) {
|
||||
html += '<td><div class="button" style="width:120px; font-weight:normal;" onMouseUp="$P().navCreateAccount()">Create Account...</div></td>';
|
||||
html += '<td width="20"> </td>';
|
||||
}
|
||||
html += '<td><div class="button" style="width:120px; font-weight:normal;" onMouseUp="$P().navPasswordRecovery()">Forgot Password...</div></td>';
|
||||
html += '<td width="20"> </td>';
|
||||
html += '<td><div class="button" style="width:120px;" onMouseUp="$P().doLogin()"><i class="fa fa-sign-in"> </i>Login</div></td>';
|
||||
html += '</tr></table></center></div>';
|
||||
html += '</div>';
|
||||
|
||||
// html += '<input type="submit" value="Login" style="position:absolute; left:-9999px; top:0px;">';
|
||||
html += '</form>';
|
||||
this.div.html( html );
|
||||
|
||||
setTimeout( function() {
|
||||
$( app.getPref('username') ? '#fe_login_password' : '#fe_login_username' ).focus();
|
||||
|
||||
$('#fe_login_username, #fe_login_password').keypress( function(event) {
|
||||
if (event.keyCode == '13') { // enter key
|
||||
event.preventDefault();
|
||||
$P().doLogin();
|
||||
}
|
||||
} );
|
||||
|
||||
}, 1 );
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/*doLoginFormSubmit: function() {
|
||||
// force login form to submit
|
||||
$('#f_login')[0].submit();
|
||||
},
|
||||
|
||||
doFrameLogin: function(resp) {
|
||||
// login from IFRAME redirect
|
||||
// alert("GOT HERE FROM IFRAME " + JSON.stringify(resp));
|
||||
this.tempFrameResp = JSON.parse( JSON.stringify(resp) );
|
||||
setTimeout( '$P().doFrameLogin2()', 1 );
|
||||
},
|
||||
|
||||
doFrameLogin2: function() {
|
||||
// login from IFRAME redirect
|
||||
var resp = this.tempFrameResp;
|
||||
delete this.tempFrameResp;
|
||||
|
||||
Debug.trace("IFRAME Response: " + JSON.stringify(resp));
|
||||
|
||||
if (resp.code) {
|
||||
return app.doError( resp.description );
|
||||
}
|
||||
|
||||
Debug.trace("IFRAME User Login: " + resp.username + ": " + resp.session_id);
|
||||
|
||||
app.clearError();
|
||||
app.hideProgress();
|
||||
app.doUserLogin( resp );
|
||||
|
||||
Nav.go( app.navAfterLogin || config.DefaultPage );
|
||||
// alert("GOT HERE: " + (app.navAfterLogin || config.DefaultPage) );
|
||||
},*/
|
||||
|
||||
doLogin: function() {
|
||||
// attempt to log user in
|
||||
var username = $('#fe_login_username').val().toLowerCase();
|
||||
var password = $('#fe_login_password').val();
|
||||
|
||||
if (username && password) {
|
||||
app.showProgress(1.0, "Logging in...");
|
||||
|
||||
app.api.post( 'user/login', {
|
||||
username: username,
|
||||
password: password
|
||||
},
|
||||
function(resp, tx) {
|
||||
Debug.trace("User Login: " + username + ": " + resp.session_id);
|
||||
|
||||
app.hideProgress();
|
||||
app.doUserLogin( resp );
|
||||
|
||||
Nav.go( app.navAfterLogin || config.DefaultPage );
|
||||
} ); // post
|
||||
}
|
||||
},
|
||||
|
||||
cancel: function() {
|
||||
// return to login page
|
||||
app.clearError();
|
||||
Nav.go('Login', true);
|
||||
},
|
||||
|
||||
navCreateAccount: function() {
|
||||
// nav to create account form
|
||||
app.clearError();
|
||||
Nav.go('Login?create=1', true);
|
||||
},
|
||||
|
||||
showCreateAccountForm: function() {
|
||||
// allow user to create a new account
|
||||
app.setWindowTitle('Create Account');
|
||||
app.showTabBar(false);
|
||||
|
||||
this.div.css({ 'padding-top':'75px', 'padding-bottom':'75px' });
|
||||
var html = '';
|
||||
|
||||
html += '<div class="inline_dialog_container">';
|
||||
html += '<div class="dialog_title shade-light">Create Account</div>';
|
||||
html += '<div class="dialog_content">';
|
||||
html += '<center><table style="margin:0px;">';
|
||||
|
||||
html += get_form_table_row( 'Username:',
|
||||
'<table cellspacing="0" cellpadding="0"><tr>' +
|
||||
'<td><input type="text" id="fe_ca_username" size="20" style="font-size:14px;" value="" spellcheck="false" onChange="$P().checkUserExists(\'ca\')"/></td>' +
|
||||
'<td><div id="d_ca_valid" style="margin-left:5px; font-weight:bold;"></div></td>' +
|
||||
'</tr></table>'
|
||||
);
|
||||
|
||||
html += get_form_table_caption('Choose a unique alphanumeric username for your account.') +
|
||||
get_form_table_spacer() +
|
||||
get_form_table_row('Password:', '<input type="' + app.get_password_type() + '" id="fe_ca_password" size="30" value="" spellcheck="false"/>' + app.get_password_toggle_html()) +
|
||||
get_form_table_caption('Enter a secure password that you will not forget.') +
|
||||
get_form_table_spacer() +
|
||||
get_form_table_row('Full Name:', '<input type="text" id="fe_ca_fullname" size="30" value="" spellcheck="false"/>') +
|
||||
get_form_table_caption('This is used for display purposes only.') +
|
||||
get_form_table_spacer() +
|
||||
get_form_table_row('Email Address:', '<input type="text" id="fe_ca_email" size="30" value="" spellcheck="false"/>') +
|
||||
get_form_table_caption('This is used only to recover your password should you lose it.');
|
||||
|
||||
html += '</table></center>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="dialog_buttons"><center><table><tr>';
|
||||
html += '<td><div class="button" style="width:120px; font-weight:normal;" onMouseUp="$P().cancel()">Cancel</div></td>';
|
||||
html += '<td width="50"> </td>';
|
||||
html += '<td><div class="button" style="width:120px;" onMouseUp="$P().doCreateAccount()"><i class="fa fa-user-plus"> </i>Create</div></td>';
|
||||
html += '</tr></table></center></div>';
|
||||
html += '</div>';
|
||||
|
||||
this.div.html( html );
|
||||
|
||||
setTimeout( function() {
|
||||
$( '#fe_ca_username' ).focus();
|
||||
app.password_strengthify( '#fe_ca_password' );
|
||||
}, 1 );
|
||||
},
|
||||
|
||||
doCreateAccount: function(force) {
|
||||
// actually create account
|
||||
app.clearError();
|
||||
|
||||
var username = trim($('#fe_ca_username').val().toLowerCase());
|
||||
var email = trim($('#fe_ca_email').val());
|
||||
var full_name = trim($('#fe_ca_fullname').val());
|
||||
var password = trim($('#fe_ca_password').val());
|
||||
|
||||
if (!username.length) {
|
||||
return app.badField('#fe_ca_username', "Please enter a username for your account.");
|
||||
}
|
||||
if (!username.match(/^[\w\-\.]+$/)) {
|
||||
return app.badField('#fe_ca_username', "Please make sure your username contains only alphanumerics, dashes and periods.");
|
||||
}
|
||||
if (!email.length) {
|
||||
return app.badField('#fe_ca_email', "Please enter an e-mail address where you can be reached.");
|
||||
}
|
||||
if (!email.match(/^\S+\@\S+$/)) {
|
||||
return app.badField('#fe_ca_email', "The e-mail address you entered does not appear to be correct.");
|
||||
}
|
||||
if (!full_name.length) {
|
||||
return app.badField('#fe_ca_fullname', "Please enter your first and last names. These are used only for display purposes.");
|
||||
}
|
||||
if (!password.length) {
|
||||
return app.badField('#fe_ca_password', "Please enter a secure password to protect your account.");
|
||||
}
|
||||
if (!force && (app.last_password_strength.score < 3)) {
|
||||
app.confirm( '<span style="color:red">Insecure Password Warning</span>', app.get_password_warning(), "Proceed", function(result) {
|
||||
if (result) $P().doCreateAccount('force');
|
||||
} );
|
||||
return;
|
||||
} // insecure password
|
||||
|
||||
Dialog.hide();
|
||||
app.showProgress( 1.0, "Creating account..." );
|
||||
|
||||
app.api.post( 'user/create', {
|
||||
username: username,
|
||||
email: email,
|
||||
password: password,
|
||||
full_name: full_name
|
||||
},
|
||||
function(resp, tx) {
|
||||
app.hideProgress();
|
||||
app.showMessage('success', "Account created successfully.");
|
||||
|
||||
app.setPref('username', username);
|
||||
Nav.go( 'Login', true );
|
||||
} ); // api.post
|
||||
},
|
||||
|
||||
navPasswordRecovery: function() {
|
||||
// nav to recover password form
|
||||
app.clearError();
|
||||
Nav.go('Login?recover=1', true);
|
||||
},
|
||||
|
||||
showRecoverPasswordForm: function() {
|
||||
// allow user to create a new account
|
||||
app.setWindowTitle('Forgot Password');
|
||||
app.showTabBar(false);
|
||||
|
||||
this.div.css({ 'padding-top':'75px', 'padding-bottom':'75px' });
|
||||
var html = '';
|
||||
|
||||
html += '<div class="inline_dialog_container">';
|
||||
html += '<div class="dialog_title shade-light">Forgot Password</div>';
|
||||
html += '<div class="dialog_content">';
|
||||
html += '<center><table style="margin:0px;">';
|
||||
|
||||
html += get_form_table_row('Username:', '<input type="text" id="fe_pr_username" size="30" value="" spellcheck="false"/>') +
|
||||
get_form_table_spacer() +
|
||||
get_form_table_row('Email Address:', '<input type="text" id="fe_pr_email" size="30" value="" spellcheck="false"/>');
|
||||
|
||||
html += '</table></center>';
|
||||
|
||||
html += '<div class="caption" style="margin-top:15px;">Please enter the username and e-mail address associated with your account, and we will send you instructions for resetting your password.</div>';
|
||||
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="dialog_buttons"><center><table><tr>';
|
||||
html += '<td><div class="button" style="width:120px; font-weight:normal;" onMouseUp="$P().cancel()">Cancel</div></td>';
|
||||
html += '<td width="50"> </td>';
|
||||
html += '<td><div class="button" style="width:120px;" onMouseUp="$P().doSendRecoveryEmail()"><i class="fa fa-envelope-o"> </i>Send Email</div></td>';
|
||||
html += '</tr></table></center></div>';
|
||||
html += '</div>';
|
||||
|
||||
this.div.html( html );
|
||||
|
||||
setTimeout( function() {
|
||||
$('#fe_pr_username, #fe_pr_email').keypress( function(event) {
|
||||
if (event.keyCode == '13') { // enter key
|
||||
event.preventDefault();
|
||||
$P().doSendEmail();
|
||||
}
|
||||
} );
|
||||
$( '#fe_pr_username' ).focus();
|
||||
}, 1 );
|
||||
},
|
||||
|
||||
doSendRecoveryEmail: function() {
|
||||
// send password recovery e-mail
|
||||
app.clearError();
|
||||
|
||||
var username = trim($('#fe_pr_username').val()).toLowerCase();
|
||||
var email = trim($('#fe_pr_email').val());
|
||||
|
||||
if (username.match(/^[\w.-]+$/)) {
|
||||
if (email.match(/.+\@.+/)) {
|
||||
Dialog.hide();
|
||||
app.showProgress( 1.0, "Sending e-mail..." );
|
||||
app.api.post( 'user/forgot_password', {
|
||||
username: username,
|
||||
email: email
|
||||
},
|
||||
function(resp, tx) {
|
||||
app.hideProgress();
|
||||
app.showMessage('success', "Password reset instructions sent successfully.");
|
||||
Nav.go('Login', true);
|
||||
} ); // api.post
|
||||
} // good address
|
||||
else app.badField('#fe_pr_email', "The e-mail address you entered does not appear to be correct.");
|
||||
} // good username
|
||||
else app.badField('#fe_pr_username', "The username you entered does not appear to be correct.");
|
||||
},
|
||||
|
||||
showPasswordResetForm: function(args) {
|
||||
// show password reset form
|
||||
this.recoveryKey = args.h;
|
||||
|
||||
app.setWindowTitle('Reset Password');
|
||||
app.showTabBar(false);
|
||||
|
||||
this.div.css({ 'padding-top':'75px', 'padding-bottom':'75px' });
|
||||
var html = '';
|
||||
|
||||
html += '<div class="inline_dialog_container">';
|
||||
html += '<div class="dialog_title shade-light">Reset Password</div>';
|
||||
html += '<div class="dialog_content">';
|
||||
html += '<center><table style="margin:0px;">';
|
||||
html += '<tr>';
|
||||
html += '<td align="right" class="table_label">Username:</td>';
|
||||
html += '<td align="left" class="table_value"><div><input type="text" name="username" id="fe_reset_username" size="30" spellcheck="false" value="'+args.u+'" disabled="disabled"/></div></td>';
|
||||
html += '</tr>';
|
||||
html += '<tr><td colspan="2"><div class="table_spacer"></div></td></tr>';
|
||||
html += '<tr>';
|
||||
html += '<td align="right" class="table_label">New Password:</td>';
|
||||
html += '<td align="left" class="table_value"><div><input type="' + app.get_password_type() + '" name="password" id="fe_reset_password" size="30" spellcheck="false" value=""/>' + app.get_password_toggle_html() + '</div></td>';
|
||||
html += '</tr>';
|
||||
html += '<tr><td colspan="2"><div class="table_spacer"></div></td></tr>';
|
||||
html += '</table></center>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="dialog_buttons"><center><table><tr>';
|
||||
html += '<td><div class="button" style="width:130px;" onMouseUp="$P().doResetPassword()"><i class="fa fa-key"> </i>Reset Password</div></td>';
|
||||
html += '</tr></table></center></div>';
|
||||
html += '</div>';
|
||||
|
||||
this.div.html( html );
|
||||
|
||||
setTimeout( function() {
|
||||
$( '#fe_reset_password' ).focus();
|
||||
$('#fe_reset_password').keypress( function(event) {
|
||||
if (event.keyCode == '13') { // enter key
|
||||
event.preventDefault();
|
||||
$P().doResetPassword();
|
||||
}
|
||||
} );
|
||||
app.password_strengthify( '#fe_reset_password' );
|
||||
}, 1 );
|
||||
},
|
||||
|
||||
doResetPassword: function(force) {
|
||||
// reset password now
|
||||
var username = $('#fe_reset_username').val().toLowerCase();
|
||||
var new_password = $('#fe_reset_password').val();
|
||||
var recovery_key = this.recoveryKey;
|
||||
|
||||
if (username && new_password) {
|
||||
if (!force && (app.last_password_strength.score < 3)) {
|
||||
app.confirm( '<span style="color:red">Insecure Password Warning</span>', app.get_password_warning(), "Proceed", function(result) {
|
||||
if (result) $P().doResetPassword('force');
|
||||
} );
|
||||
return;
|
||||
} // insecure password
|
||||
|
||||
app.showProgress(1.0, "Resetting password...");
|
||||
|
||||
app.api.post( 'user/reset_password', {
|
||||
username: username,
|
||||
key: recovery_key,
|
||||
new_password: new_password
|
||||
},
|
||||
function(resp, tx) {
|
||||
Debug.trace("User password was reset: " + username);
|
||||
|
||||
app.hideProgress();
|
||||
app.setPref('username', username);
|
||||
|
||||
Nav.go( 'Login', true );
|
||||
|
||||
setTimeout( function() {
|
||||
app.showMessage('success', "Your password was reset successfully.");
|
||||
}, 100 );
|
||||
} ); // post
|
||||
}
|
||||
},
|
||||
|
||||
onDeactivate: function() {
|
||||
// called when page is deactivated
|
||||
this.div.html( '' );
|
||||
return true;
|
||||
}
|
||||
|
||||
} );
|
181
workspaces/base-workspace/Cronicle-0.8.61/htdocs/js/pages/MyAccount.class.js
Executable file
|
@ -0,0 +1,181 @@
|
|||
Class.subclass( Page.Base, "Page.MyAccount", {
|
||||
|
||||
onInit: function() {
|
||||
// called once at page load
|
||||
var html = '';
|
||||
this.div.html( html );
|
||||
},
|
||||
|
||||
onActivate: function(args) {
|
||||
// page activation
|
||||
if (!this.requireLogin(args)) return true;
|
||||
|
||||
if (!args) args = {};
|
||||
this.args = args;
|
||||
|
||||
app.setWindowTitle('My Account');
|
||||
app.showTabBar(true);
|
||||
|
||||
this.receive_user({ user: app.user });
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
receive_user: function(resp, tx) {
|
||||
var self = this;
|
||||
var html = '';
|
||||
var user = resp.user;
|
||||
|
||||
html += '<div style="padding:50px 20px 50px 20px">';
|
||||
html += '<center>';
|
||||
|
||||
html += '<table><tr>';
|
||||
html += '<td valign="top" style="vertical-align:top">';
|
||||
|
||||
html += '<table style="margin:0;">';
|
||||
|
||||
// user id
|
||||
html += get_form_table_row( 'Username', '<div style="font-size: 14px;"><b>' + app.username + '</b></div>' );
|
||||
html += get_form_table_caption( "Your username cannot be changed." );
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// full name
|
||||
html += get_form_table_row( 'Full Name', '<input type="text" id="fe_ma_fullname" size="30" value="'+escape_text_field_value(user.full_name)+'"/>' );
|
||||
html += get_form_table_caption( "Your first and last names, used for display purposes only.");
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// email
|
||||
html += get_form_table_row( 'Email Address', '<input type="text" id="fe_ma_email" size="30" value="'+escape_text_field_value(user.email)+'"/>' );
|
||||
html += get_form_table_caption( "This is used to generate your profile pic, and to<br/>recover your password if you forget it." );
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// current password
|
||||
html += get_form_table_row( 'Current Password', '<input type="' + app.get_password_type() + '" id="fe_ma_old_password" size="30" value="" spellcheck="false"/>' + app.get_password_toggle_html() );
|
||||
html += get_form_table_caption( "Enter your current account password to make changes." );
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// reset password
|
||||
html += get_form_table_row( 'New Password', '<input type="' + app.get_password_type() + '" id="fe_ma_new_password" size="30" value="" spellcheck="false"/>' + app.get_password_toggle_html() );
|
||||
html += get_form_table_caption( "If you need to change your password, enter the new one here." );
|
||||
html += get_form_table_spacer();
|
||||
|
||||
html += '<tr><td colspan="2" align="center">';
|
||||
html += '<div style="height:30px;"></div>';
|
||||
|
||||
html += '<table><tr>';
|
||||
html += '<td><div class="button" style="width:130px; font-weight:normal;" onMouseUp="$P().show_delete_account_dialog()">Delete Account...</div></td>';
|
||||
html += '<td width="80"> </td>';
|
||||
html += '<td><div class="button" style="width:130px;" onMouseUp="$P().save_changes()"><i class="fa fa-floppy-o"> </i>Save Changes</div></td>';
|
||||
html += '</tr></table>';
|
||||
|
||||
html += '</td></tr>';
|
||||
|
||||
html += '</table>';
|
||||
html += '</center>';
|
||||
|
||||
html += '</td>';
|
||||
html += '<td valign="top" align="left" style="vertical-align:top; text-align:left;">';
|
||||
// gravar profile image and edit button
|
||||
html += '<fieldset style="width:150px; margin-left:40px; background:white; border:1px solid #ddd; box-shadow:none;"><legend>Profile Picture</legend>';
|
||||
if (app.config.external_users) {
|
||||
html += '<div id="d_ma_image" style="width:128px; height:128px; margin:5px auto 0 auto; background-image:url('+app.getUserAvatarURL(128)+'); cursor:default;"></div>';
|
||||
}
|
||||
else {
|
||||
html += '<div id="d_ma_image" style="width:128px; height:128px; margin:5px auto 0 auto; background-image:url('+app.getUserAvatarURL(128)+'); cursor:pointer;" onMouseUp="$P().edit_gravatar()"></div>';
|
||||
html += '<div class="button mini" style="margin:10px auto 5px auto;" onMouseUp="$P().edit_gravatar()">Edit Image...</div>';
|
||||
html += '<div style="font-size:11px; color:#888; text-align:center; margin-bottom:5px;">Image services provided by <a href="https://en.gravatar.com/connect/" target="_blank">Gravatar.com</a>.</div>';
|
||||
}
|
||||
html += '</fieldset>';
|
||||
html += '</td>';
|
||||
html += '</tr></table>';
|
||||
|
||||
html += '</div>'; // table wrapper div
|
||||
|
||||
this.div.html( html );
|
||||
|
||||
setTimeout( function() {
|
||||
app.password_strengthify( '#fe_ma_new_password' );
|
||||
|
||||
if (app.config.external_users) {
|
||||
app.showMessage('warning', "Users are managed by an external system, so you cannot make changes here.");
|
||||
self.div.find('input').prop('disabled', true);
|
||||
}
|
||||
}, 1 );
|
||||
},
|
||||
|
||||
edit_gravatar: function() {
|
||||
// edit profile pic at gravatar.com
|
||||
window.open( 'https://en.gravatar.com/connect/' );
|
||||
},
|
||||
|
||||
save_changes: function(force) {
|
||||
// save changes to user info
|
||||
app.clearError();
|
||||
if (app.config.external_users) {
|
||||
return app.doError("Users are managed by an external system, so you cannot make changes here.");
|
||||
}
|
||||
if (!$('#fe_ma_old_password').val()) return app.badField('#fe_ma_old_password', "Please enter your current account password to make changes.");
|
||||
|
||||
if ($('#fe_ma_new_password').val() && !force && (app.last_password_strength.score < 3)) {
|
||||
app.confirm( '<span style="color:red">Insecure Password Warning</span>', app.get_password_warning(), "Proceed", function(result) {
|
||||
if (result) $P().save_changes('force');
|
||||
} );
|
||||
return;
|
||||
} // insecure password
|
||||
|
||||
app.showProgress( 1.0, "Saving account..." );
|
||||
|
||||
app.api.post( 'user/update', {
|
||||
username: app.username,
|
||||
full_name: trim($('#fe_ma_fullname').val()),
|
||||
email: trim($('#fe_ma_email').val()),
|
||||
old_password: $('#fe_ma_old_password').val(),
|
||||
new_password: $('#fe_ma_new_password').val()
|
||||
},
|
||||
function(resp) {
|
||||
// save complete
|
||||
app.hideProgress();
|
||||
app.showMessage('success', "Your account settings were updated successfully.");
|
||||
|
||||
$('#fe_ma_old_password').val('');
|
||||
$('#fe_ma_new_password').val('');
|
||||
|
||||
app.user = resp.user;
|
||||
app.updateHeaderInfo();
|
||||
|
||||
$('#d_ma_image').css( 'background-image', 'url('+app.getUserAvatarURL(128)+')' );
|
||||
} );
|
||||
},
|
||||
|
||||
show_delete_account_dialog: function() {
|
||||
// show dialog confirming account delete action
|
||||
var self = this;
|
||||
|
||||
app.clearError();
|
||||
if (app.config.external_users) {
|
||||
return app.doError("Users are managed by an external system, so you cannot make changes here.");
|
||||
}
|
||||
if (!$('#fe_ma_old_password').val()) return app.badField('#fe_ma_old_password', "Please enter your current account password.");
|
||||
|
||||
app.confirm( "Delete My Account", "Are you sure you want to <b>permanently delete</b> your user account? There is no way to undo this action, and no way to recover your data.", "Delete", function(result) {
|
||||
if (result) {
|
||||
app.showProgress( 1.0, "Deleting Account..." );
|
||||
app.api.post( 'user/delete', {
|
||||
username: app.username,
|
||||
password: $('#fe_ma_old_password').val()
|
||||
},
|
||||
function(resp) {
|
||||
// finished deleting, immediately log user out
|
||||
app.doUserLogout();
|
||||
} );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
onDeactivate: function() {
|
||||
// called when page is deactivated
|
||||
// this.div.html( '' );
|
||||
return true;
|
||||
}
|
||||
|
||||
} );
|
1816
workspaces/base-workspace/Cronicle-0.8.61/htdocs/js/pages/Schedule.class.js
Executable file
340
workspaces/base-workspace/Cronicle-0.8.61/htdocs/js/pages/admin/APIKeys.js
Executable file
|
@ -0,0 +1,340 @@
|
|||
// Cronicle Admin Page -- API Keys
|
||||
|
||||
Class.add( Page.Admin, {
|
||||
|
||||
gosub_api_keys: function(args) {
|
||||
// show API Key list
|
||||
app.setWindowTitle( "API Keys" );
|
||||
this.div.addClass('loading');
|
||||
app.api.post( 'app/get_api_keys', copy_object(args), this.receive_keys.bind(this) );
|
||||
},
|
||||
|
||||
receive_keys: function(resp) {
|
||||
// receive all API Keys from server, render them sorted
|
||||
this.lastAPIKeysResp = resp;
|
||||
|
||||
var html = '';
|
||||
this.div.removeClass('loading');
|
||||
|
||||
var size = get_inner_window_size();
|
||||
var col_width = Math.floor( ((size.width * 0.9) + 200) / 7 );
|
||||
|
||||
if (!resp.rows) resp.rows = [];
|
||||
|
||||
// sort by title ascending
|
||||
this.api_keys = resp.rows.sort( function(a, b) {
|
||||
return a.title.toLowerCase().localeCompare( b.title.toLowerCase() );
|
||||
} );
|
||||
|
||||
html += this.getSidebarTabs( 'api_keys',
|
||||
[
|
||||
['activity', "Activity Log"],
|
||||
['api_keys', "API Keys"],
|
||||
['categories', "Categories"],
|
||||
['plugins', "Plugins"],
|
||||
['servers', "Servers"],
|
||||
['users', "Users"]
|
||||
]
|
||||
);
|
||||
|
||||
var cols = ['App Title', 'API Key', 'Status', 'Author', 'Created', 'Actions'];
|
||||
|
||||
html += '<div style="padding:20px 20px 30px 20px">';
|
||||
|
||||
html += '<div class="subtitle">';
|
||||
html += 'API Keys';
|
||||
html += '<div class="clear"></div>';
|
||||
html += '</div>';
|
||||
|
||||
var self = this;
|
||||
html += this.getBasicTable( this.api_keys, cols, 'key', function(item, idx) {
|
||||
var actions = [
|
||||
'<span class="link" onMouseUp="$P().edit_api_key('+idx+')"><b>Edit</b></span>',
|
||||
'<span class="link" onMouseUp="$P().delete_api_key('+idx+')"><b>Delete</b></span>'
|
||||
];
|
||||
return [
|
||||
'<div class="td_big">' + self.getNiceAPIKey(item, true, col_width) + '</div>',
|
||||
'<div style="">' + encode_entities(item.key) + '</div>',
|
||||
item.active ? '<span class="color_label green"><i class="fa fa-check"> </i>Active</span>' : '<span class="color_label red"><i class="fa fa-warning"> </i>Suspended</span>',
|
||||
self.getNiceUsername(item.username, true, col_width),
|
||||
'<span title="'+get_nice_date_time(item.created, true)+'">'+get_nice_date(item.created, true)+'</span>',
|
||||
actions.join(' | ')
|
||||
];
|
||||
} );
|
||||
|
||||
html += '<div style="height:30px;"></div>';
|
||||
html += '<center><table><tr>';
|
||||
html += '<td><div class="button" style="width:130px;" onMouseUp="$P().edit_api_key(-1)"><i class="fa fa-plus-circle"> </i>Add API Key...</div></td>';
|
||||
html += '</tr></table></center>';
|
||||
|
||||
html += '</div>'; // padding
|
||||
html += '</div>'; // sidebar tabs
|
||||
|
||||
this.div.html( html );
|
||||
},
|
||||
|
||||
edit_api_key: function(idx) {
|
||||
// jump to edit sub
|
||||
if (idx > -1) Nav.go( '#Admin?sub=edit_api_key&id=' + this.api_keys[idx].id );
|
||||
else Nav.go( '#Admin?sub=new_api_key' );
|
||||
},
|
||||
|
||||
delete_api_key: function(idx) {
|
||||
// delete key from search results
|
||||
this.api_key = this.api_keys[idx];
|
||||
this.show_delete_api_key_dialog();
|
||||
},
|
||||
|
||||
gosub_new_api_key: function(args) {
|
||||
// create new API Key
|
||||
var html = '';
|
||||
app.setWindowTitle( "New API Key" );
|
||||
this.div.removeClass('loading');
|
||||
|
||||
html += this.getSidebarTabs( 'new_api_key',
|
||||
[
|
||||
['activity', "Activity Log"],
|
||||
['api_keys', "API Keys"],
|
||||
['new_api_key', "New API Key"],
|
||||
['categories', "Categories"],
|
||||
['plugins', "Plugins"],
|
||||
['servers', "Servers"],
|
||||
['users', "Users"]
|
||||
]
|
||||
);
|
||||
|
||||
html += '<div style="padding:20px;"><div class="subtitle">New API Key</div></div>';
|
||||
|
||||
html += '<div style="padding:0px 20px 50px 20px">';
|
||||
html += '<center><table style="margin:0;">';
|
||||
|
||||
this.api_key = { privileges: {}, key: get_unique_id() };
|
||||
|
||||
html += this.get_api_key_edit_html();
|
||||
|
||||
// buttons at bottom
|
||||
html += '<tr><td colspan="2" align="center">';
|
||||
html += '<div style="height:30px;"></div>';
|
||||
|
||||
html += '<table><tr>';
|
||||
html += '<td><div class="button" style="width:120px; font-weight:normal;" onMouseUp="$P().cancel_api_key_edit()">Cancel</div></td>';
|
||||
html += '<td width="50"> </td>';
|
||||
|
||||
html += '<td><div class="button" style="width:120px;" onMouseUp="$P().do_new_api_key()"><i class="fa fa-plus-circle"> </i>Create Key</div></td>';
|
||||
html += '</tr></table>';
|
||||
|
||||
html += '</td></tr>';
|
||||
|
||||
html += '</table></center>';
|
||||
html += '</div>'; // table wrapper div
|
||||
|
||||
html += '</div>'; // sidebar tabs
|
||||
|
||||
this.div.html( html );
|
||||
|
||||
setTimeout( function() {
|
||||
$('#fe_ak_title').focus();
|
||||
}, 1 );
|
||||
},
|
||||
|
||||
cancel_api_key_edit: function() {
|
||||
// cancel editing API Key and return to list
|
||||
Nav.go( 'Admin?sub=api_keys' );
|
||||
},
|
||||
|
||||
do_new_api_key: function(force) {
|
||||
// create new API Key
|
||||
app.clearError();
|
||||
var api_key = this.get_api_key_form_json();
|
||||
if (!api_key) return; // error
|
||||
|
||||
if (!api_key.title.length) {
|
||||
return app.badField('#fe_ak_title', "Please enter an app title for the new API Key.");
|
||||
}
|
||||
|
||||
this.api_key = api_key;
|
||||
|
||||
app.showProgress( 1.0, "Creating API Key..." );
|
||||
app.api.post( 'app/create_api_key', api_key, this.new_api_key_finish.bind(this) );
|
||||
},
|
||||
|
||||
new_api_key_finish: function(resp) {
|
||||
// new API Key created successfully
|
||||
app.hideProgress();
|
||||
|
||||
Nav.go('Admin?sub=edit_api_key&id=' + resp.id);
|
||||
|
||||
setTimeout( function() {
|
||||
app.showMessage('success', "The new API Key was created successfully.");
|
||||
}, 150 );
|
||||
},
|
||||
|
||||
gosub_edit_api_key: function(args) {
|
||||
// edit API Key subpage
|
||||
this.div.addClass('loading');
|
||||
app.api.post( 'app/get_api_key', { id: args.id }, this.receive_key.bind(this) );
|
||||
},
|
||||
|
||||
receive_key: function(resp) {
|
||||
// edit existing API Key
|
||||
var html = '';
|
||||
this.api_key = resp.api_key;
|
||||
|
||||
app.setWindowTitle( "Editing API Key \"" + (this.api_key.title) + "\"" );
|
||||
this.div.removeClass('loading');
|
||||
|
||||
html += this.getSidebarTabs( 'edit_api_key',
|
||||
[
|
||||
['activity', "Activity Log"],
|
||||
['api_keys', "API Keys"],
|
||||
['edit_api_key', "Edit API Key"],
|
||||
['categories', "Categories"],
|
||||
['plugins', "Plugins"],
|
||||
['servers', "Servers"],
|
||||
['users', "Users"]
|
||||
]
|
||||
);
|
||||
|
||||
html += '<div style="padding:20px;"><div class="subtitle">Editing API Key “' + (this.api_key.title) + '”</div></div>';
|
||||
|
||||
html += '<div style="padding:0px 20px 50px 20px">';
|
||||
html += '<center>';
|
||||
html += '<table style="margin:0;">';
|
||||
|
||||
html += this.get_api_key_edit_html();
|
||||
|
||||
html += '<tr><td colspan="2" align="center">';
|
||||
html += '<div style="height:30px;"></div>';
|
||||
|
||||
html += '<table><tr>';
|
||||
html += '<td><div class="button" style="width:130px; font-weight:normal;" onMouseUp="$P().cancel_api_key_edit()">Cancel</div></td>';
|
||||
html += '<td width="50"> </td>';
|
||||
html += '<td><div class="button" style="width:130px; font-weight:normal;" onMouseUp="$P().show_delete_api_key_dialog()">Delete Key...</div></td>';
|
||||
html += '<td width="50"> </td>';
|
||||
html += '<td><div class="button" style="width:130px;" onMouseUp="$P().do_save_api_key()"><i class="fa fa-floppy-o"> </i>Save Changes</div></td>';
|
||||
html += '</tr></table>';
|
||||
|
||||
html += '</td></tr>';
|
||||
|
||||
html += '</table>';
|
||||
html += '</center>';
|
||||
html += '</div>'; // table wrapper div
|
||||
|
||||
html += '</div>'; // sidebar tabs
|
||||
|
||||
this.div.html( html );
|
||||
},
|
||||
|
||||
do_save_api_key: function() {
|
||||
// save changes to api key
|
||||
app.clearError();
|
||||
var api_key = this.get_api_key_form_json();
|
||||
if (!api_key) return; // error
|
||||
|
||||
this.api_key = api_key;
|
||||
|
||||
app.showProgress( 1.0, "Saving API Key..." );
|
||||
app.api.post( 'app/update_api_key', api_key, this.save_api_key_finish.bind(this) );
|
||||
},
|
||||
|
||||
save_api_key_finish: function(resp, tx) {
|
||||
// new API Key saved successfully
|
||||
app.hideProgress();
|
||||
app.showMessage('success', "The API Key was saved successfully.");
|
||||
window.scrollTo( 0, 0 );
|
||||
},
|
||||
|
||||
show_delete_api_key_dialog: function() {
|
||||
// show dialog confirming api key delete action
|
||||
var self = this;
|
||||
app.confirm( '<span style="color:red">Delete API Key</span>', "Are you sure you want to <b>permanently delete</b> the API Key \""+this.api_key.title+"\"? There is no way to undo this action.", 'Delete', function(result) {
|
||||
if (result) {
|
||||
app.showProgress( 1.0, "Deleting API Key..." );
|
||||
app.api.post( 'app/delete_api_key', self.api_key, self.delete_api_key_finish.bind(self) );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
delete_api_key_finish: function(resp, tx) {
|
||||
// finished deleting API Key
|
||||
var self = this;
|
||||
app.hideProgress();
|
||||
|
||||
Nav.go('Admin?sub=api_keys', 'force');
|
||||
|
||||
setTimeout( function() {
|
||||
app.showMessage('success', "The API Key '"+self.api_key.title+"' was deleted successfully.");
|
||||
}, 150 );
|
||||
},
|
||||
|
||||
get_api_key_edit_html: function() {
|
||||
// get html for editing an API Key (or creating a new one)
|
||||
var html = '';
|
||||
var api_key = this.api_key;
|
||||
|
||||
// API Key
|
||||
html += get_form_table_row( 'API Key', '<input type="text" id="fe_ak_key" size="35" value="'+escape_text_field_value(api_key.key)+'" spellcheck="false"/> <span class="link addme" onMouseUp="$P().generate_key()">« Generate Random</span>' );
|
||||
html += get_form_table_caption( "The API Key string is used to authenticate API calls." );
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// status
|
||||
html += get_form_table_row( 'Status', '<select id="fe_ak_status">' + render_menu_options([[1,'Active'], [0,'Disabled']], api_key.active) + '</select>' );
|
||||
html += get_form_table_caption( "'Disabled' means that the API Key remains in the system, but it cannot be used for any API calls." );
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// title
|
||||
html += get_form_table_row( 'App Title', '<input type="text" id="fe_ak_title" size="30" value="'+escape_text_field_value(api_key.title)+'" spellcheck="false"/>' );
|
||||
html += get_form_table_caption( "Enter the title of the application that will be using the API Key.");
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// description
|
||||
html += get_form_table_row('App Description', '<textarea id="fe_ak_desc" style="width:550px; height:50px; resize:vertical;">'+escape_text_field_value(api_key.description)+'</textarea>');
|
||||
html += get_form_table_caption( "Optionally enter a more detailed description of the application." );
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// privilege list
|
||||
var priv_html = '';
|
||||
for (var idx = 0, len = config.privilege_list.length; idx < len; idx++) {
|
||||
var priv = config.privilege_list[idx];
|
||||
if (priv.id != 'admin') {
|
||||
var has_priv = !!api_key.privileges[ priv.id ];
|
||||
priv_html += '<div style="margin-top:4px; margin-bottom:4px;">';
|
||||
priv_html += '<input type="checkbox" id="fe_ak_priv_'+priv.id+'" value="1" '+(has_priv ? 'checked="checked"' : '')+'>';
|
||||
priv_html += '<label for="fe_ak_priv_'+priv.id+'">'+priv.title+'</label>';
|
||||
priv_html += '</div>';
|
||||
}
|
||||
}
|
||||
html += get_form_table_row( 'Privileges', priv_html );
|
||||
html += get_form_table_caption( "Select which privileges the API Key should have." );
|
||||
html += get_form_table_spacer();
|
||||
|
||||
return html;
|
||||
},
|
||||
|
||||
get_api_key_form_json: function() {
|
||||
// get api key elements from form, used for new or edit
|
||||
var api_key = this.api_key;
|
||||
|
||||
api_key.key = $('#fe_ak_key').val();
|
||||
api_key.active = $('#fe_ak_status').val();
|
||||
api_key.title = $('#fe_ak_title').val();
|
||||
api_key.description = $('#fe_ak_desc').val();
|
||||
|
||||
if (!api_key.key.length) {
|
||||
return app.badField('#fe_ak_key', "Please enter an API Key string, or generate a random one.");
|
||||
}
|
||||
|
||||
for (var idx = 0, len = config.privilege_list.length; idx < len; idx++) {
|
||||
var priv = config.privilege_list[idx];
|
||||
api_key.privileges[ priv.id ] = $('#fe_ak_priv_'+priv.id).is(':checked') ? 1 : 0;
|
||||
}
|
||||
|
||||
return api_key;
|
||||
},
|
||||
|
||||
generate_key: function() {
|
||||
// generate random api key
|
||||
$('#fe_ak_key').val( get_unique_id() );
|
||||
}
|
||||
|
||||
});
|
246
workspaces/base-workspace/Cronicle-0.8.61/htdocs/js/pages/admin/Activity.js
Executable file
|
@ -0,0 +1,246 @@
|
|||
// Cronicle Admin Page -- Activity Log
|
||||
|
||||
Class.add( Page.Admin, {
|
||||
|
||||
activity_types: {
|
||||
'^cat': '<i class="fa fa-folder-open-o"> </i>Category',
|
||||
'^group': '<i class="mdi mdi-server-network"> </i>Group',
|
||||
'^plugin': '<i class="fa fa-plug"> </i>Plugin',
|
||||
// '^apikey': '<i class="fa fa-key"> </i>API Key',
|
||||
'^apikey': '<i class="mdi mdi-key-variant"> </i>API Key',
|
||||
'^event': '<i class="fa fa-clock-o"> </i>Event',
|
||||
'^user': '<i class="fa fa-user"> </i>User',
|
||||
'server': '<i class="mdi mdi-desktop-tower mdi-lg"> </i>Server',
|
||||
'^job': '<i class="fa fa-pie-chart"> </i>Job',
|
||||
'^state': '<i class="mdi mdi-calendar-clock"> </i>Scheduler', // mdi-lg
|
||||
'^error': '<i class="fa fa-exclamation-triangle"> </i>Error',
|
||||
'^warning': '<i class="fa fa-exclamation-circle"> </i>Warning'
|
||||
},
|
||||
|
||||
gosub_activity: function(args) {
|
||||
// show activity log
|
||||
app.setWindowTitle( "Activity Log" );
|
||||
|
||||
if (!args.offset) args.offset = 0;
|
||||
if (!args.limit) args.limit = 25;
|
||||
app.api.post( 'app/get_activity', copy_object(args), this.receive_activity.bind(this) );
|
||||
},
|
||||
|
||||
receive_activity: function(resp) {
|
||||
// receive page of activity from server, render it
|
||||
this.lastActivityResp = resp;
|
||||
|
||||
var html = '';
|
||||
this.div.removeClass('loading');
|
||||
|
||||
html += this.getSidebarTabs( 'activity',
|
||||
[
|
||||
['activity', "Activity Log"],
|
||||
['api_keys', "API Keys"],
|
||||
['categories', "Categories"],
|
||||
['plugins', "Plugins"],
|
||||
['servers', "Servers"],
|
||||
['users', "Users"]
|
||||
]
|
||||
);
|
||||
|
||||
this.events = [];
|
||||
if (resp.rows) this.events = resp.rows;
|
||||
|
||||
var cols = ['Date/Time', 'Type', 'Description', 'Username', 'IP Address', 'Actions'];
|
||||
|
||||
html += '<div style="padding:20px 20px 30px 20px">';
|
||||
|
||||
html += '<div class="subtitle">';
|
||||
html += 'Activity Log';
|
||||
// html += '<div class="clear"></div>';
|
||||
html += '</div>';
|
||||
|
||||
var self = this;
|
||||
html += this.getPaginatedTable( resp, cols, 'item', function(item, idx) {
|
||||
// figure out icon first
|
||||
if (!item.action) item.action = 'unknown';
|
||||
|
||||
var item_type = '';
|
||||
for (var key in self.activity_types) {
|
||||
var regexp = new RegExp(key);
|
||||
if (item.action.match(regexp)) {
|
||||
item_type = self.activity_types[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// compose nice description
|
||||
var desc = '';
|
||||
var actions = [];
|
||||
var color = '';
|
||||
|
||||
switch (item.action) {
|
||||
|
||||
// categories
|
||||
case 'cat_create':
|
||||
desc = 'New category created: <b>' + item.cat.title + '</b>';
|
||||
break;
|
||||
case 'cat_update':
|
||||
desc = 'Category updated: <b>' + item.cat.title + '</b>';
|
||||
break;
|
||||
case 'cat_delete':
|
||||
desc = 'Category deleted: <b>' + item.cat.title + '</b>';
|
||||
break;
|
||||
|
||||
// groups
|
||||
case 'group_create':
|
||||
desc = 'New server group created: <b>' + item.group.title + '</b>';
|
||||
break;
|
||||
case 'group_update':
|
||||
desc = 'Server group updated: <b>' + item.group.title + '</b>';
|
||||
break;
|
||||
case 'group_delete':
|
||||
desc = 'Server group deleted: <b>' + item.group.title + '</b>';
|
||||
break;
|
||||
|
||||
// plugins
|
||||
case 'plugin_create':
|
||||
desc = 'New Plugin created: <b>' + item.plugin.title + '</b>';
|
||||
break;
|
||||
case 'plugin_update':
|
||||
desc = 'Plugin updated: <b>' + item.plugin.title + '</b>';
|
||||
break;
|
||||
case 'plugin_delete':
|
||||
desc = 'Plugin deleted: <b>' + item.plugin.title + '</b>';
|
||||
break;
|
||||
|
||||
// api keys
|
||||
case 'apikey_create':
|
||||
desc = 'New API Key created: <b>' + item.api_key.title + '</b> (Key: ' + item.api_key.key + ')';
|
||||
actions.push( '<a href="#Admin?sub=edit_api_key&id='+item.api_key.id+'">Edit Key</a>' );
|
||||
break;
|
||||
case 'apikey_update':
|
||||
desc = 'API Key updated: <b>' + item.api_key.title + '</b> (Key: ' + item.api_key.key + ')';
|
||||
actions.push( '<a href="#Admin?sub=edit_api_key&id='+item.api_key.id+'">Edit Key</a>' );
|
||||
break;
|
||||
case 'apikey_delete':
|
||||
desc = 'API Key deleted: <b>' + item.api_key.title + '</b> (Key: ' + item.api_key.key + ')';
|
||||
break;
|
||||
|
||||
// events
|
||||
case 'event_create':
|
||||
desc = 'New event added: <b>' + item.event.title + '</b>';
|
||||
desc += " (" + summarize_event_timing(item.event.timing, item.event.timezone) + ")";
|
||||
actions.push( '<a href="#Schedule?sub=edit_event&id='+item.event.id+'">Edit Event</a>' );
|
||||
break;
|
||||
case 'event_update':
|
||||
desc = 'Event updated: <b>' + item.event.title + '</b>';
|
||||
actions.push( '<a href="#Schedule?sub=edit_event&id='+item.event.id+'">Edit Event</a>' );
|
||||
break;
|
||||
case 'event_delete':
|
||||
desc = 'Event deleted: <b>' + item.event.title + '</b>';
|
||||
break;
|
||||
|
||||
// users
|
||||
case 'user_create':
|
||||
desc = 'New user account created: <b>' + item.user.username + "</b> (" + item.user.full_name + ")";
|
||||
actions.push( '<a href="#Admin?sub=edit_user&username='+item.user.username+'">Edit User</a>' );
|
||||
break;
|
||||
case 'user_update':
|
||||
desc = 'User account updated: <b>' + item.user.username + "</b> (" + item.user.full_name + ")";
|
||||
actions.push( '<a href="#Admin?sub=edit_user&username='+item.user.username+'">Edit User</a>' );
|
||||
break;
|
||||
case 'user_delete':
|
||||
desc = 'User account deleted: <b>' + item.user.username + "</b> (" + item.user.full_name + ")";
|
||||
break;
|
||||
case 'user_login':
|
||||
desc = "User logged in: <b>" + item.user.username + "</b> (" + item.user.full_name + ")";
|
||||
break;
|
||||
|
||||
// servers
|
||||
case 'add_server': // legacy
|
||||
case 'server_add': // current
|
||||
desc = 'Server '+(item.manual ? 'manually ' : '')+'added to cluster: <b>' + item.hostname + '</b>';
|
||||
break;
|
||||
case 'remove_server': // legacy
|
||||
case 'server_remove': // current
|
||||
desc = 'Server '+(item.manual ? 'manually ' : '')+'removed from cluster: <b>' + item.hostname + '</b>';
|
||||
break;
|
||||
case 'master_server': // legacy
|
||||
case 'server_master': // current
|
||||
desc = 'Server has become primary: <b>' + item.hostname + '</b>';
|
||||
break;
|
||||
|
||||
case 'server_restart':
|
||||
desc = 'Server restarted: <b>' + item.hostname + '</b>';
|
||||
break;
|
||||
case 'server_shutdown':
|
||||
desc = 'Server shut down: <b>' + item.hostname + '</b>';
|
||||
break;
|
||||
|
||||
case 'server_disable':
|
||||
desc = 'Lost connectivity to server: <b>' + item.hostname + '</b>';
|
||||
color = 'yellow';
|
||||
break;
|
||||
case 'server_enable':
|
||||
desc = 'Reconnected to server: <b>' + item.hostname + '</b>';
|
||||
break;
|
||||
|
||||
// jobs
|
||||
case 'job_run':
|
||||
var event = find_object( app.schedule, { id: item.event } ) || { title: 'Unknown Event' };
|
||||
desc = 'Job <b>#'+item.id+'</b> ('+event.title+') manually started';
|
||||
actions.push( '<a href="#JobDetails?id='+item.id+'">Job Details</a>' );
|
||||
break;
|
||||
case 'job_complete':
|
||||
var event = find_object( app.schedule, { id: item.event } ) || { title: 'Unknown Event' };
|
||||
if (!item.code) {
|
||||
desc = 'Job <b>#'+item.id+'</b> ('+event.title+') on server <b>'+item.hostname.replace(/\.[\w\-]+\.\w+$/, '')+'</b> completed successfully';
|
||||
}
|
||||
else {
|
||||
desc = 'Job <b>#'+item.id+'</b> ('+event.title+') on server <b>'+item.hostname.replace(/\.[\w\-]+\.\w+$/, '')+'</b> failed with error: ' + encode_entities(item.description || 'Unknown Error');
|
||||
if (desc.match(/\n/)) desc = desc.split(/\n/).shift() + "...";
|
||||
color = 'red';
|
||||
}
|
||||
actions.push( '<a href="#JobDetails?id='+item.id+'">Job Details</a>' );
|
||||
break;
|
||||
case 'job_delete':
|
||||
var event = find_object( app.schedule, { id: item.event } ) || { title: 'Unknown Event' };
|
||||
desc = 'Job <b>#'+item.id+'</b> ('+event.title+') manually deleted';
|
||||
break;
|
||||
|
||||
// scheduler
|
||||
case 'state_update':
|
||||
desc = 'Scheduler was <b>' + (item.enabled ? 'enabled' : 'disabled') + '</b>';
|
||||
break;
|
||||
|
||||
// errors
|
||||
case 'error':
|
||||
desc = encode_entities( item.description );
|
||||
color = 'red';
|
||||
break;
|
||||
|
||||
// warnings
|
||||
case 'warning':
|
||||
desc = encode_entities( item.description );
|
||||
color = 'yellow';
|
||||
break;
|
||||
|
||||
} // action
|
||||
|
||||
var tds = [
|
||||
'<div style="white-space:nowrap;">' + get_nice_date_time( item.epoch || 0, false, true ) + '</div>',
|
||||
'<div class="td_big" style="white-space:nowrap; font-size:12px; font-weight:normal;">' + item_type + '</div>',
|
||||
'<div class="activity_desc">' + desc + '</div>',
|
||||
'<div style="white-space:nowrap;">' + self.getNiceUsername(item, true) + '</div>',
|
||||
(item.ip || 'n/a').replace(/^\:\:ffff\:(\d+\.\d+\.\d+\.\d+)$/, '$1'),
|
||||
'<div style="white-space:nowrap;">' + actions.join(' | ') + '</div>'
|
||||
];
|
||||
if (color) tds.className = color;
|
||||
|
||||
return tds;
|
||||
} );
|
||||
|
||||
html += '</div>'; // padding
|
||||
html += '</div>'; // sidebar tabs
|
||||
|
||||
this.div.html( html );
|
||||
}
|
||||
|
||||
});
|
495
workspaces/base-workspace/Cronicle-0.8.61/htdocs/js/pages/admin/Categories.js
Executable file
|
@ -0,0 +1,495 @@
|
|||
// Cronicle Admin Page -- Categories
|
||||
|
||||
Class.add( Page.Admin, {
|
||||
|
||||
gosub_categories: function(args) {
|
||||
// show category list
|
||||
this.div.removeClass('loading');
|
||||
app.setWindowTitle( "Categories" );
|
||||
|
||||
var size = get_inner_window_size();
|
||||
var col_width = Math.floor( ((size.width * 0.9) + 200) / 5 );
|
||||
|
||||
var html = '';
|
||||
|
||||
html += this.getSidebarTabs( 'categories',
|
||||
[
|
||||
['activity', "Activity Log"],
|
||||
['api_keys', "API Keys"],
|
||||
['categories', "Categories"],
|
||||
['plugins', "Plugins"],
|
||||
['servers', "Servers"],
|
||||
['users', "Users"]
|
||||
]
|
||||
);
|
||||
|
||||
var cols = ['Title', 'Description', 'Assigned Events', 'Max Concurrent', 'Actions'];
|
||||
|
||||
html += '<div style="padding:20px 20px 30px 20px">';
|
||||
|
||||
html += '<div class="subtitle">';
|
||||
html += 'Event Categories';
|
||||
// html += '<div class="clear"></div>';
|
||||
html += '</div>';
|
||||
|
||||
// sort by title ascending
|
||||
this.categories = app.categories.sort( function(a, b) {
|
||||
// return (b.title < a.title) ? 1 : -1;
|
||||
return a.title.toLowerCase().localeCompare( b.title.toLowerCase() );
|
||||
} );
|
||||
|
||||
// render table
|
||||
var self = this;
|
||||
html += this.getBasicTable( this.categories, cols, 'category', function(cat, idx) {
|
||||
var actions = [
|
||||
'<span class="link" onMouseUp="$P().edit_category('+idx+')"><b>Edit</b></span>',
|
||||
'<span class="link" onMouseUp="$P().delete_category('+idx+')"><b>Delete</b></span>'
|
||||
];
|
||||
|
||||
var cat_events = find_objects( app.schedule, { category: cat.id } );
|
||||
var num_events = cat_events.length;
|
||||
|
||||
var tds = [
|
||||
'<div class="td_big"><span class="link" onMouseUp="$P().edit_category('+idx+')">' + self.getNiceCategory(cat, col_width) + '</span></div>',
|
||||
'<div class="ellip" style="max-width:'+col_width+'px;">' + encode_entities(cat.description || '(No description)') + '</div>',
|
||||
num_events ? commify( num_events ) : '(None)',
|
||||
cat.max_children ? commify(cat.max_children) : '(No limit)',
|
||||
actions.join(' | ')
|
||||
];
|
||||
|
||||
if (cat && cat.color) {
|
||||
if (tds.className) tds.className += ' '; else tds.className = '';
|
||||
tds.className += cat.color;
|
||||
}
|
||||
|
||||
if (!cat.enabled) {
|
||||
if (tds.className) tds.className += ' '; else tds.className = '';
|
||||
tds.className += 'disabled';
|
||||
}
|
||||
|
||||
return tds;
|
||||
} );
|
||||
|
||||
html += '<div style="height:30px;"></div>';
|
||||
html += '<center><table><tr>';
|
||||
html += '<td><div class="button" style="width:130px;" onMouseUp="$P().edit_category(-1)"><i class="fa fa-plus-circle"> </i>Add Category...</div></td>';
|
||||
html += '</tr></table></center>';
|
||||
|
||||
html += '</div>'; // padding
|
||||
html += '</div>'; // sidebar tabs
|
||||
|
||||
this.div.html( html );
|
||||
},
|
||||
|
||||
edit_category: function(idx) {
|
||||
// jump to edit sub
|
||||
if (idx > -1) Nav.go( '#Admin?sub=edit_category&id=' + this.categories[idx].id );
|
||||
else Nav.go( '#Admin?sub=new_category' );
|
||||
},
|
||||
|
||||
delete_category: function(idx) {
|
||||
// delete key from search results
|
||||
this.category = this.categories[idx];
|
||||
this.show_delete_category_dialog();
|
||||
},
|
||||
|
||||
gosub_new_category: function(args) {
|
||||
// create new Category
|
||||
var html = '';
|
||||
app.setWindowTitle( "New Category" );
|
||||
this.div.removeClass('loading');
|
||||
|
||||
html += this.getSidebarTabs( 'new_category',
|
||||
[
|
||||
['activity', "Activity Log"],
|
||||
['api_keys', "API Keys"],
|
||||
['categories', "Categories"],
|
||||
['new_category', "New Category"],
|
||||
['plugins', "Plugins"],
|
||||
['servers', "Servers"],
|
||||
['users', "Users"]
|
||||
]
|
||||
);
|
||||
|
||||
html += '<div style="padding:20px;"><div class="subtitle">Add New Category</div></div>';
|
||||
|
||||
html += '<div style="padding:0px 20px 50px 20px">';
|
||||
html += '<center><table style="margin:0;">';
|
||||
|
||||
this.category = {
|
||||
title: "",
|
||||
description: "",
|
||||
max_children: 0,
|
||||
enabled: 1
|
||||
};
|
||||
|
||||
html += this.get_category_edit_html();
|
||||
|
||||
// buttons at bottom
|
||||
html += '<tr><td colspan="2" align="center">';
|
||||
html += '<div style="height:30px;"></div>';
|
||||
|
||||
html += '<table><tr>';
|
||||
html += '<td><div class="button" style="width:120px; font-weight:normal;" onMouseUp="$P().cancel_category_edit()">Cancel</div></td>';
|
||||
html += '<td width="50"> </td>';
|
||||
|
||||
html += '<td><div class="button" style="width:120px;" onMouseUp="$P().do_new_category()"><i class="fa fa-plus-circle"> </i>Add Category</div></td>';
|
||||
html += '</tr></table>';
|
||||
|
||||
html += '</td></tr>';
|
||||
|
||||
html += '</table></center>';
|
||||
html += '</div>'; // table wrapper div
|
||||
|
||||
html += '</div>'; // sidebar tabs
|
||||
|
||||
this.div.html( html );
|
||||
|
||||
setTimeout( function() {
|
||||
$('#fe_ec_title').focus();
|
||||
}, 1 );
|
||||
},
|
||||
|
||||
cancel_category_edit: function() {
|
||||
// cancel editing category and return to list
|
||||
Nav.go( 'Admin?sub=categories' );
|
||||
},
|
||||
|
||||
do_new_category: function(force) {
|
||||
// create new category
|
||||
app.clearError();
|
||||
var category = this.get_category_form_json();
|
||||
if (!category) return; // error
|
||||
|
||||
// pro-tip: embed id in title as bracketed prefix
|
||||
if (category.title.match(/^\[(\w+)\]\s*(.+)$/)) {
|
||||
category.id = RegExp.$1;
|
||||
category.title = RegExp.$2;
|
||||
}
|
||||
|
||||
this.category = category;
|
||||
|
||||
app.showProgress( 1.0, "Creating category..." );
|
||||
app.api.post( 'app/create_category', category, this.new_category_finish.bind(this) );
|
||||
},
|
||||
|
||||
new_category_finish: function(resp) {
|
||||
// new Category created successfully
|
||||
app.hideProgress();
|
||||
|
||||
// Can't nav to edit_category yet, websocket may not have received update yet
|
||||
// Nav.go('Admin?sub=edit_category&id=' + resp.id);
|
||||
Nav.go('Admin?sub=categories');
|
||||
|
||||
setTimeout( function() {
|
||||
app.showMessage('success', "The new category was created successfully.");
|
||||
}, 150 );
|
||||
},
|
||||
|
||||
gosub_edit_category: function(args) {
|
||||
// edit existing Category
|
||||
var html = '';
|
||||
this.category = find_object( app.categories, { id: args.id } );
|
||||
|
||||
app.setWindowTitle( "Editing Category \"" + (this.category.title) + "\"" );
|
||||
this.div.removeClass('loading');
|
||||
|
||||
html += this.getSidebarTabs( 'edit_category',
|
||||
[
|
||||
['activity', "Activity Log"],
|
||||
['api_keys', "API Keys"],
|
||||
['categories', "Categories"],
|
||||
['edit_category', "Edit Category"],
|
||||
['plugins', "Plugins"],
|
||||
['servers', "Servers"],
|
||||
['users', "Users"]
|
||||
]
|
||||
);
|
||||
|
||||
html += '<div style="padding:20px;"><div class="subtitle">Editing Category “' + (this.category.title) + '”</div></div>';
|
||||
|
||||
html += '<div style="padding:0px 20px 50px 20px">';
|
||||
html += '<center>';
|
||||
html += '<table style="margin:0;">';
|
||||
|
||||
html += this.get_category_edit_html();
|
||||
|
||||
html += '<tr><td colspan="2" align="center">';
|
||||
html += '<div style="height:30px;"></div>';
|
||||
|
||||
html += '<table><tr>';
|
||||
html += '<td><div class="button" style="width:130px; font-weight:normal;" onMouseUp="$P().cancel_category_edit()">Cancel</div></td>';
|
||||
html += '<td width="50"> </td>';
|
||||
html += '<td><div class="button" style="width:130px; font-weight:normal;" onMouseUp="$P().show_delete_category_dialog()">Delete Category...</div></td>';
|
||||
html += '<td width="50"> </td>';
|
||||
html += '<td><div class="button" style="width:130px;" onMouseUp="$P().do_save_category()"><i class="fa fa-floppy-o"> </i>Save Changes</div></td>';
|
||||
html += '</tr></table>';
|
||||
|
||||
html += '</td></tr>';
|
||||
|
||||
html += '</table>';
|
||||
html += '</center>';
|
||||
html += '</div>'; // table wrapper div
|
||||
|
||||
html += '</div>'; // sidebar tabs
|
||||
|
||||
this.div.html( html );
|
||||
},
|
||||
|
||||
do_save_category: function() {
|
||||
// save changes to category
|
||||
app.clearError();
|
||||
var category = this.get_category_form_json();
|
||||
if (!category) return; // error
|
||||
|
||||
this.category = category;
|
||||
|
||||
app.showProgress( 1.0, "Saving category..." );
|
||||
app.api.post( 'app/update_category', category, this.save_category_finish.bind(this) );
|
||||
},
|
||||
|
||||
save_category_finish: function(resp, tx) {
|
||||
// new category saved successfully
|
||||
var self = this;
|
||||
var category = this.category;
|
||||
|
||||
app.hideProgress();
|
||||
app.showMessage('success', "The category was saved successfully.");
|
||||
window.scrollTo( 0, 0 );
|
||||
|
||||
// copy active jobs to array
|
||||
var jobs = [];
|
||||
for (var id in app.activeJobs) {
|
||||
var job = app.activeJobs[id];
|
||||
if ((job.category == category.id) && !job.detached) jobs.push( job );
|
||||
}
|
||||
|
||||
// if the cat was disabled and there are running jobs, ask user to abort them
|
||||
if (!category.enabled && jobs.length) {
|
||||
app.confirm( '<span style="color:red">Abort Jobs</span>', "There " + ((jobs.length != 1) ? 'are' : 'is') + " currently still " + jobs.length + " active " + pluralize('job', jobs.length) + " using the disabled category <b>"+category.title+"</b>. Do you want to abort " + ((jobs.length != 1) ? 'these' : 'it') + " now?", "Abort", function(result) {
|
||||
if (result) {
|
||||
app.showProgress( 1.0, "Aborting " + pluralize('Job', jobs.length) + "..." );
|
||||
app.api.post( 'app/abort_jobs', { category: category.id }, function(resp) {
|
||||
app.hideProgress();
|
||||
if (resp.count > 0) {
|
||||
app.showMessage('success', "The " + pluralize('job', resp.count) + " " + ((resp.count != 1) ? 'were' : 'was') + " aborted successfully.");
|
||||
}
|
||||
else {
|
||||
app.showMessage('warning', "No jobs were aborted. It is likely they completed while the dialog was up.");
|
||||
}
|
||||
} );
|
||||
} // clicked Abort
|
||||
} ); // app.confirm
|
||||
} // disabled + jobs
|
||||
},
|
||||
|
||||
show_delete_category_dialog: function() {
|
||||
// show dialog confirming category delete action
|
||||
var self = this;
|
||||
var category = this.category;
|
||||
var cat = this.category;
|
||||
|
||||
// check for events first
|
||||
var cat_events = find_objects( app.schedule, { category: cat.id } );
|
||||
var num_events = cat_events.length;
|
||||
if (num_events) return app.doError("Sorry, you cannot delete a category that has events assigned to it.");
|
||||
|
||||
// proceed with delete
|
||||
var self = this;
|
||||
app.confirm( '<span style="color:red">Delete Category</span>', "Are you sure you want to delete the category <b>"+cat.title+"</b>? There is no way to undo this action.", "Delete", function(result) {
|
||||
if (result) {
|
||||
app.showProgress( 1.0, "Deleting Category..." );
|
||||
app.api.post( 'app/delete_category', cat, self.delete_category_finish.bind(self) );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
delete_category_finish: function(resp, tx) {
|
||||
// finished deleting category
|
||||
var self = this;
|
||||
app.hideProgress();
|
||||
|
||||
Nav.go('Admin?sub=categories', 'force');
|
||||
|
||||
setTimeout( function() {
|
||||
app.showMessage('success', "The category '"+self.category.title+"' was deleted successfully.");
|
||||
}, 150 );
|
||||
},
|
||||
|
||||
get_category_edit_html: function() {
|
||||
// get html for editing a category (or creating a new one)
|
||||
var html = '';
|
||||
var category = this.category;
|
||||
var cat = this.category;
|
||||
|
||||
// Internal ID
|
||||
if (cat.id && this.isAdmin()) {
|
||||
html += get_form_table_row( 'Category ID', '<div style="font-size:14px;">' + cat.id + '</div>' );
|
||||
html += get_form_table_caption( "The internal Category ID used for API calls. This cannot be changed." );
|
||||
html += get_form_table_spacer();
|
||||
}
|
||||
|
||||
// title
|
||||
html += get_form_table_row('Category Title:', '<input type="text" id="fe_ec_title" size="25" value="'+escape_text_field_value(cat.title)+'"/>') +
|
||||
get_form_table_caption("Enter a title for the category, short and sweet.") +
|
||||
get_form_table_spacer();
|
||||
|
||||
// cat enabled
|
||||
html += get_form_table_row( 'Active', '<input type="checkbox" id="fe_ec_enabled" value="1" ' + (cat.enabled ? 'checked="checked"' : '') + '/><label for="fe_ec_enabled">Category Enabled</label>' );
|
||||
html += get_form_table_caption( "Select whether events in this category should be enabled or disabled in the schedule." );
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// description
|
||||
html += get_form_table_row('Description:', '<textarea id="fe_ec_desc" style="width:500px; height:50px; resize:vertical;">'+escape_text_field_value(cat.description)+'</textarea>') +
|
||||
get_form_table_caption("Optionally enter a description for the category.") +
|
||||
get_form_table_spacer();
|
||||
|
||||
// max concurrent
|
||||
html += get_form_table_row('Max Concurrent:', '<select id="fe_ec_max_children">' + render_menu_options([ [0,'No Limit'], 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 ], cat.max_children, true) + '</select>') +
|
||||
get_form_table_caption("Select the maximum number of jobs allowed to run concurrently in this category.");
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// color
|
||||
var current_color = cat.color || 'plain';
|
||||
var swatch_html = '';
|
||||
var colors = ['plain', 'red', 'green', 'blue', 'skyblue', 'yellow', 'purple', 'orange'];
|
||||
for (var idx = 0, len = colors.length; idx < len; idx++) {
|
||||
var color = colors[idx];
|
||||
swatch_html += '<div class="swatch ' + color + ' ' + ((current_color == color) ? 'active' : '') + '" onMouseUp="$P().select_color(\''+color+'\')"></div>';
|
||||
}
|
||||
swatch_html += '<div class="clear"></div>';
|
||||
|
||||
html += get_form_table_row( 'Highlight Color', swatch_html );
|
||||
html += get_form_table_caption( "Optionally select a highlight color for the category, which will show on the schedule." );
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// default notification options
|
||||
var notif_expanded = !!(cat.notify_success || cat.notify_fail || cat.web_hook);
|
||||
html += get_form_table_row( 'Notification',
|
||||
'<div style="font-size:13px;'+(notif_expanded ? 'display:none;' : '')+'"><span class="link addme" onMouseUp="$P().expand_fieldset($(this))"><i class="fa fa-plus-square-o"> </i>Default Notification Options</span></div>' +
|
||||
'<fieldset style="padding:10px 10px 0 10px; margin-bottom:5px;'+(notif_expanded ? '' : 'display:none;')+'"><legend class="link addme" onMouseUp="$P().collapse_fieldset($(this))"><i class="fa fa-minus-square-o"> </i>Default Notification Options</legend>' +
|
||||
'<div class="plugin_params_label">Default Email on Success:</div>' +
|
||||
'<div class="plugin_params_content"><input type="text" id="fe_ec_notify_success" size="50" value="'+escape_text_field_value(cat.notify_success)+'" placeholder="email@sample.com" spellcheck="false" onChange="$P().update_add_remove_me($(this))"/><span class="link addme" onMouseUp="$P().add_remove_me($(this).prev())"></span></div>' +
|
||||
|
||||
'<div class="plugin_params_label">Default Email on Failure:</div>' +
|
||||
'<div class="plugin_params_content"><input type="text" id="fe_ec_notify_fail" size="50" value="'+escape_text_field_value(cat.notify_fail)+'" placeholder="email@sample.com" spellcheck="false" onChange="$P().update_add_remove_me($(this))"/><span class="link addme" onMouseUp="$P().add_remove_me($(this).prev())"></span></div>' +
|
||||
|
||||
'<div class="plugin_params_label">Default Web Hook URL:</div>' +
|
||||
'<div class="plugin_params_content"><input type="text" id="fe_ec_web_hook" size="60" value="'+escape_text_field_value(cat.web_hook)+'" placeholder="http://" spellcheck="false"/></div>' +
|
||||
'</fieldset>'
|
||||
);
|
||||
html += get_form_table_caption( "Optionally enter default e-mail addresses for notification, and/or a web hook URL.<br/>Note that events can override any of these notification settings." );
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// default resource limits
|
||||
var res_expanded = !!(cat.memory_limit || cat.memory_sustain || cat.cpu_limit || cat.cpu_sustain || cat.log_max_size);
|
||||
html += get_form_table_row( 'Limits',
|
||||
'<div style="font-size:13px;'+(res_expanded ? 'display:none;' : '')+'"><span class="link addme" onMouseUp="$P().expand_fieldset($(this))"><i class="fa fa-plus-square-o"> </i>Default Resource Limits</span></div>' +
|
||||
'<fieldset style="padding:10px 10px 0 10px; margin-bottom:5px;'+(res_expanded ? '' : 'display:none;')+'"><legend class="link addme" onMouseUp="$P().collapse_fieldset($(this))"><i class="fa fa-minus-square-o"> </i>Default Resource Limits</legend>' +
|
||||
|
||||
'<div class="plugin_params_label">Default CPU Limit:</div>' +
|
||||
'<div class="plugin_params_content"><table cellspacing="0" cellpadding="0" class="fieldset_params_table"><tr>' +
|
||||
'<td style="padding-right:2px"><input type="checkbox" id="fe_ec_cpu_enabled" value="1" '+(cat.cpu_limit ? 'checked="checked"' : '')+' /></td>' +
|
||||
'<td><label for="fe_ec_cpu_enabled">Abort job if CPU exceeds</label></td>' +
|
||||
'<td><input type="text" id="fe_ec_cpu_limit" style="width:30px;" value="'+(cat.cpu_limit || 0)+'"/>%</td>' +
|
||||
'<td>for</td>' +
|
||||
'<td>' + this.get_relative_time_combo_box( 'fe_ec_cpu_sustain', cat.cpu_sustain, 'fieldset_params_table' ) + '</td>' +
|
||||
'</tr></table></div>' +
|
||||
|
||||
'<div class="plugin_params_label">Default Memory Limit:</div>' +
|
||||
'<div class="plugin_params_content"><table cellspacing="0" cellpadding="0" class="fieldset_params_table"><tr>' +
|
||||
'<td style="padding-right:2px"><input type="checkbox" id="fe_ec_memory_enabled" value="1" '+(cat.memory_limit ? 'checked="checked"' : '')+' /></td>' +
|
||||
'<td><label for="fe_ec_memory_enabled">Abort job if memory exceeds</label></td>' +
|
||||
'<td>' + this.get_relative_size_combo_box( 'fe_ec_memory_limit', cat.memory_limit, 'fieldset_params_table' ) + '</td>' +
|
||||
'<td>for</td>' +
|
||||
'<td>' + this.get_relative_time_combo_box( 'fe_ec_memory_sustain', cat.memory_sustain, 'fieldset_params_table' ) + '</td>' +
|
||||
'</tr></table></div>' +
|
||||
|
||||
'<div class="plugin_params_label">Default Log Size Limit:</div>' +
|
||||
'<div class="plugin_params_content"><table cellspacing="0" cellpadding="0" class="fieldset_params_table"><tr>' +
|
||||
'<td style="padding-right:2px"><input type="checkbox" id="fe_ec_log_enabled" value="1" '+(cat.log_max_size ? 'checked="checked"' : '')+' /></td>' +
|
||||
'<td><label for="fe_ec_log_enabled">Abort job if log file exceeds</label></td>' +
|
||||
'<td>' + this.get_relative_size_combo_box( 'fe_ec_log_limit', cat.log_max_size, 'fieldset_params_table' ) + '</td>' +
|
||||
'</tr></table></div>' +
|
||||
|
||||
'</fieldset>'
|
||||
);
|
||||
html += get_form_table_caption(
|
||||
"Optionally set default CPU load, memory usage and log size limits for the category.<br/>Note that events can override any of these limits."
|
||||
);
|
||||
html += get_form_table_spacer();
|
||||
|
||||
setTimeout( function() {
|
||||
$P().update_add_remove_me( $('#fe_ec_notify_success, #fe_ec_notify_fail') );
|
||||
}, 1 );
|
||||
|
||||
return html;
|
||||
},
|
||||
|
||||
select_color: function(color) {
|
||||
// click on a color swatch
|
||||
this.category.color = (color == 'plain') ? '' : color;
|
||||
$('.swatch').removeClass('active');
|
||||
$('.swatch.'+color).addClass('active');
|
||||
},
|
||||
|
||||
get_category_form_json: function() {
|
||||
// get category elements from form, used for new or edit
|
||||
var category = this.category;
|
||||
|
||||
category.title = $('#fe_ec_title').val();
|
||||
if (!category.title.length) {
|
||||
return app.badField('#fe_ec_title', "Please enter a title for the category.");
|
||||
}
|
||||
|
||||
category.enabled = $('#fe_ec_enabled').is(':checked') ? 1 : 0;
|
||||
category.description = $('#fe_ec_desc').val();
|
||||
category.max_children = parseInt( $('#fe_ec_max_children').val() );
|
||||
category.notify_success = $('#fe_ec_notify_success').val();
|
||||
category.notify_fail = $('#fe_ec_notify_fail').val();
|
||||
category.web_hook = $('#fe_ec_web_hook').val();
|
||||
|
||||
// cpu limit
|
||||
if ($('#fe_ec_cpu_enabled').is(':checked')) {
|
||||
category.cpu_limit = parseInt( $('#fe_ec_cpu_limit').val() );
|
||||
if (isNaN(category.cpu_limit)) return app.badField('fe_ec_cpu_limit', "Please enter an integer value for the CPU limit.");
|
||||
if (category.cpu_limit < 0) return app.badField('fe_ec_cpu_limit', "Please enter a positive integer for the CPU limit.");
|
||||
|
||||
category.cpu_sustain = parseInt( $('#fe_ec_cpu_sustain').val() ) * parseInt( $('#fe_ec_cpu_sustain_units').val() );
|
||||
if (isNaN(category.cpu_sustain)) return app.badField('fe_ec_cpu_sustain', "Please enter an integer value for the CPU sustain period.");
|
||||
if (category.cpu_sustain < 0) return app.badField('fe_ec_cpu_sustain', "Please enter a positive integer for the CPU sustain period.");
|
||||
}
|
||||
else {
|
||||
category.cpu_limit = 0;
|
||||
category.cpu_sustain = 0;
|
||||
}
|
||||
|
||||
// mem limit
|
||||
if ($('#fe_ec_memory_enabled').is(':checked')) {
|
||||
category.memory_limit = parseInt( $('#fe_ec_memory_limit').val() ) * parseInt( $('#fe_ec_memory_limit_units').val() );
|
||||
if (isNaN(category.memory_limit)) return app.badField('fe_ec_memory_limit', "Please enter an integer value for the memory limit.");
|
||||
if (category.memory_limit < 0) return app.badField('fe_ec_memory_limit', "Please enter a positive integer for the memory limit.");
|
||||
|
||||
category.memory_sustain = parseInt( $('#fe_ec_memory_sustain').val() ) * parseInt( $('#fe_ec_memory_sustain_units').val() );
|
||||
if (isNaN(category.memory_sustain)) return app.badField('fe_ec_memory_sustain', "Please enter an integer value for the memory sustain period.");
|
||||
if (category.memory_sustain < 0) return app.badField('fe_ec_memory_sustain', "Please enter a positive integer for the memory sustain period.");
|
||||
}
|
||||
else {
|
||||
category.memory_limit = 0;
|
||||
category.memory_sustain = 0;
|
||||
}
|
||||
|
||||
// job log file size limit
|
||||
if ($('#fe_ec_log_enabled').is(':checked')) {
|
||||
category.log_max_size = parseInt( $('#fe_ec_log_limit').val() ) * parseInt( $('#fe_ec_log_limit_units').val() );
|
||||
if (isNaN(category.log_max_size)) return app.badField('fe_ec_log_limit', "Please enter an integer value for the log size limit.");
|
||||
if (category.log_max_size < 0) return app.badField('fe_ec_log_limit', "Please enter a positive integer for the log size limit.");
|
||||
}
|
||||
else {
|
||||
category.log_max_size = 0;
|
||||
}
|
||||
|
||||
return category;
|
||||
}
|
||||
|
||||
});
|
686
workspaces/base-workspace/Cronicle-0.8.61/htdocs/js/pages/admin/Plugins.js
Executable file
|
@ -0,0 +1,686 @@
|
|||
// Cronicle Admin Page -- Plugins
|
||||
|
||||
Class.add( Page.Admin, {
|
||||
|
||||
ctype_labels: {
|
||||
text: "Text Field",
|
||||
textarea: "Text Box",
|
||||
checkbox: "Checkbox",
|
||||
hidden: "Hidden",
|
||||
select: "Menu"
|
||||
},
|
||||
|
||||
gosub_plugins: function(args) {
|
||||
// show plugin list
|
||||
this.div.removeClass('loading');
|
||||
app.setWindowTitle( "Plugins" );
|
||||
|
||||
var size = get_inner_window_size();
|
||||
var col_width = Math.floor( ((size.width * 0.9) + 500) / 6 );
|
||||
|
||||
var html = '';
|
||||
|
||||
this.plugins = app.plugins;
|
||||
|
||||
html += this.getSidebarTabs( 'plugins',
|
||||
[
|
||||
['activity', "Activity Log"],
|
||||
['api_keys', "API Keys"],
|
||||
['categories', "Categories"],
|
||||
['plugins', "Plugins"],
|
||||
['servers', "Servers"],
|
||||
['users', "Users"]
|
||||
]
|
||||
);
|
||||
|
||||
var cols = ['Plugin Name', 'Author', '# of Events', 'Created', 'Modified', 'Actions'];
|
||||
|
||||
// html += '<div style="padding:5px 15px 15px 15px;">';
|
||||
html += '<div style="padding:20px 20px 30px 20px">';
|
||||
|
||||
html += '<div class="subtitle">';
|
||||
html += 'Plugins';
|
||||
// html += '<div class="clear"></div>';
|
||||
html += '</div>';
|
||||
|
||||
// sort by title ascending
|
||||
this.plugins = app.plugins.sort( function(a, b) {
|
||||
// return (b.title < a.title) ? 1 : -1;
|
||||
return a.title.toLowerCase().localeCompare( b.title.toLowerCase() );
|
||||
} );
|
||||
|
||||
var self = this;
|
||||
html += this.getBasicTable( this.plugins, cols, 'plugin', function(plugin, idx) {
|
||||
var actions = [
|
||||
'<span class="link" onMouseUp="$P().edit_plugin('+idx+')"><b>Edit</b></span>',
|
||||
'<span class="link" onMouseUp="$P().delete_plugin('+idx+')"><b>Delete</b></span>'
|
||||
];
|
||||
|
||||
var plugin_events = find_objects( app.schedule, { plugin: plugin.id } );
|
||||
var num_events = plugin_events.length;
|
||||
|
||||
var tds = [
|
||||
'<div class="td_big"><a href="#Admin?sub=edit_plugin&id='+plugin.id+'">' + self.getNicePlugin(plugin, col_width) + '</a></div>',
|
||||
self.getNiceUsername(plugin, true, col_width),
|
||||
num_events ? commify( num_events ) : '(None)',
|
||||
'<span title="'+get_nice_date_time(plugin.created, true)+'">'+get_nice_date(plugin.created, true)+'</span>',
|
||||
'<span title="'+get_nice_date_time(plugin.modified, true)+'">'+get_nice_date(plugin.modified, true)+'</span>',
|
||||
actions.join(' | ')
|
||||
];
|
||||
|
||||
if (!plugin.enabled) {
|
||||
if (tds.className) tds.className += ' '; else tds.className = '';
|
||||
tds.className += 'disabled';
|
||||
}
|
||||
|
||||
return tds;
|
||||
} );
|
||||
|
||||
html += '<div style="height:30px;"></div>';
|
||||
html += '<center><table><tr>';
|
||||
html += '<td><div class="button" style="width:140px;" onMouseUp="$P().edit_plugin(-1)"><i class="fa fa-plus-circle"> </i>Add New Plugin...</div></td>';
|
||||
html += '</tr></table></center>';
|
||||
|
||||
html += '</div>'; // padding
|
||||
html += '</div>'; // sidebar tabs
|
||||
|
||||
this.div.html( html );
|
||||
},
|
||||
|
||||
edit_plugin: function(idx) {
|
||||
// jump to edit sub
|
||||
if (idx > -1) Nav.go( '#Admin?sub=edit_plugin&id=' + this.plugins[idx].id );
|
||||
else Nav.go( '#Admin?sub=new_plugin' );
|
||||
},
|
||||
|
||||
delete_plugin: function(idx) {
|
||||
// delete key from search results
|
||||
this.plugin = this.plugins[idx];
|
||||
this.show_delete_plugin_dialog();
|
||||
},
|
||||
|
||||
show_delete_plugin_dialog: function() {
|
||||
// delete selected plugin
|
||||
var plugin = this.plugin;
|
||||
|
||||
// check for events first
|
||||
var plugin_events = find_objects( app.schedule, { plugin: plugin.id } );
|
||||
var num_events = plugin_events.length;
|
||||
if (num_events) return app.doError("Sorry, you cannot delete a plugin that has events assigned to it.");
|
||||
|
||||
// proceed with delete
|
||||
var self = this;
|
||||
app.confirm( '<span style="color:red">Delete Plugin</span>', "Are you sure you want to delete the plugin <b>"+plugin.title+"</b>? There is no way to undo this action.", "Delete", function(result) {
|
||||
if (result) {
|
||||
app.showProgress( 1.0, "Deleting Plugin..." );
|
||||
app.api.post( 'app/delete_plugin', plugin, function(resp) {
|
||||
app.hideProgress();
|
||||
app.showMessage('success', "The Plugin '"+self.plugin.title+"' was deleted successfully.");
|
||||
// self.gosub_plugins(self.args);
|
||||
|
||||
Nav.go('Admin?sub=plugins', 'force');
|
||||
} );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
gosub_new_plugin: function(args) {
|
||||
// create new plugin
|
||||
var html = '';
|
||||
app.setWindowTitle( "Add New Plugin" );
|
||||
this.div.removeClass('loading');
|
||||
|
||||
html += this.getSidebarTabs( 'new_plugin',
|
||||
[
|
||||
['activity', "Activity Log"],
|
||||
['api_keys', "API Keys"],
|
||||
['categories', "Categories"],
|
||||
['plugins', "Plugins"],
|
||||
['new_plugin', "Add New Plugin"],
|
||||
['servers', "Servers"],
|
||||
['users', "Users"]
|
||||
]
|
||||
);
|
||||
|
||||
html += '<div style="padding:20px;"><div class="subtitle">Add New Plugin</div></div>';
|
||||
|
||||
html += '<div style="padding:0px 20px 50px 20px">';
|
||||
html += '<center><table style="margin:0;">';
|
||||
|
||||
if (this.plugin_copy) {
|
||||
this.plugin = this.plugin_copy;
|
||||
delete this.plugin_copy;
|
||||
}
|
||||
else {
|
||||
this.plugin = { params: [], enabled: 1 };
|
||||
}
|
||||
|
||||
html += this.get_plugin_edit_html();
|
||||
|
||||
// buttons at bottom
|
||||
html += '<tr><td colspan="2" align="center">';
|
||||
html += '<div style="height:30px;"></div>';
|
||||
|
||||
html += '<table><tr>';
|
||||
html += '<td><div class="button" style="width:120px; font-weight:normal;" onMouseUp="$P().cancel_plugin_edit()">Cancel</div></td>';
|
||||
html += '<td width="50"> </td>';
|
||||
html += '<td><div class="button" style="width:120px;" onMouseUp="$P().do_new_plugin()"><i class="fa fa-plus-circle"> </i>Create Plugin</div></td>';
|
||||
html += '</tr></table>';
|
||||
|
||||
html += '</td></tr>';
|
||||
html += '</table></center>';
|
||||
|
||||
html += '</div>'; // table wrapper div
|
||||
html += '</div>'; // sidebar tabs
|
||||
|
||||
this.div.html( html );
|
||||
|
||||
setTimeout( function() {
|
||||
$('#fe_ep_title').focus();
|
||||
}, 1 );
|
||||
},
|
||||
|
||||
cancel_plugin_edit: function() {
|
||||
// cancel edit, nav back to plugin list
|
||||
Nav.go('Admin?sub=plugins');
|
||||
},
|
||||
|
||||
do_new_plugin: function(force) {
|
||||
// create new plugin
|
||||
app.clearError();
|
||||
var plugin = this.get_plugin_form_json();
|
||||
if (!plugin) return; // error
|
||||
|
||||
// pro-tip: embed id in title as bracketed prefix
|
||||
if (plugin.title.match(/^\[(\w+)\]\s*(.+)$/)) {
|
||||
plugin.id = RegExp.$1;
|
||||
plugin.title = RegExp.$2;
|
||||
}
|
||||
|
||||
this.plugin = plugin;
|
||||
|
||||
app.showProgress( 1.0, "Creating plugin..." );
|
||||
app.api.post( 'app/create_plugin', plugin, this.new_plugin_finish.bind(this) );
|
||||
},
|
||||
|
||||
new_plugin_finish: function(resp) {
|
||||
// new plugin created successfully
|
||||
app.hideProgress();
|
||||
|
||||
Nav.go('Admin?sub=plugins');
|
||||
|
||||
setTimeout( function() {
|
||||
app.showMessage('success', "The new plugin was created successfully.");
|
||||
}, 150 );
|
||||
},
|
||||
|
||||
gosub_edit_plugin: function(args) {
|
||||
// edit plugin subpage
|
||||
var plugin = find_object( app.plugins, { id: args.id } );
|
||||
if (!plugin) return app.doError("Could not locate Plugin with ID: " + args.id);
|
||||
|
||||
// make local copy so edits don't affect main app list until save
|
||||
this.plugin = deep_copy_object( plugin );
|
||||
|
||||
var html = '';
|
||||
app.setWindowTitle( "Editing Plugin \"" + plugin.title + "\"" );
|
||||
this.div.removeClass('loading');
|
||||
|
||||
html += this.getSidebarTabs( 'edit_plugin',
|
||||
[
|
||||
['activity', "Activity Log"],
|
||||
['api_keys', "API Keys"],
|
||||
['categories', "Categories"],
|
||||
['plugins', "Plugins"],
|
||||
['edit_plugin', "Edit Plugin"],
|
||||
['servers', "Servers"],
|
||||
['users', "Users"]
|
||||
]
|
||||
);
|
||||
|
||||
html += '<div style="padding:20px;"><div class="subtitle">Editing Plugin “' + plugin.title + '”</div></div>';
|
||||
|
||||
html += '<div style="padding:0px 20px 50px 20px">';
|
||||
html += '<center>';
|
||||
html += '<table style="margin:0;">';
|
||||
|
||||
html += this.get_plugin_edit_html();
|
||||
|
||||
html += '<tr><td colspan="2" align="center">';
|
||||
html += '<div style="height:30px;"></div>';
|
||||
|
||||
html += '<table><tr>';
|
||||
html += '<td><div class="button" style="width:120px; font-weight:normal;" onMouseUp="$P().cancel_plugin_edit()">Cancel</div></td>';
|
||||
html += '<td width="50"> </td>';
|
||||
html += '<td><div class="button" style="width:120px; font-weight:normal;" onMouseUp="$P().show_delete_plugin_dialog()">Delete Plugin...</div></td>';
|
||||
html += '<td width="50"> </td>';
|
||||
html += '<td><div class="button" style="width:120px; font-weight:normal;" onMouseUp="$P().do_copy_plugin()">Copy Plugin...</div></td>';
|
||||
html += '<td width="50"> </td>';
|
||||
html += '<td><div class="button" style="width:130px;" onMouseUp="$P().do_save_plugin()"><i class="fa fa-floppy-o"> </i>Save Changes</div></td>';
|
||||
html += '</tr></table>';
|
||||
|
||||
html += '</td></tr>';
|
||||
|
||||
html += '</table>';
|
||||
html += '</center>';
|
||||
html += '</div>'; // table wrapper div
|
||||
|
||||
html += '</div>'; // sidebar tabs
|
||||
|
||||
this.div.html( html );
|
||||
},
|
||||
|
||||
do_copy_plugin: function() {
|
||||
// copy plugin to new
|
||||
app.clearError();
|
||||
var plugin = this.get_plugin_form_json();
|
||||
if (!plugin) return; // error
|
||||
|
||||
delete plugin.id;
|
||||
delete plugin.created;
|
||||
delete plugin.modified;
|
||||
delete plugin.username;
|
||||
|
||||
plugin.title = "Copy of " + plugin.title;
|
||||
|
||||
this.plugin_copy = plugin;
|
||||
Nav.go('Admin?sub=new_plugin');
|
||||
},
|
||||
|
||||
do_save_plugin: function() {
|
||||
// save changes to existing plugin
|
||||
app.clearError();
|
||||
var plugin = this.get_plugin_form_json();
|
||||
if (!plugin) return; // error
|
||||
|
||||
this.plugin = plugin;
|
||||
|
||||
app.showProgress( 1.0, "Saving plugin..." );
|
||||
app.api.post( 'app/update_plugin', plugin, this.save_plugin_finish.bind(this) );
|
||||
},
|
||||
|
||||
save_plugin_finish: function(resp, tx) {
|
||||
// existing plugin saved successfully
|
||||
var self = this;
|
||||
var plugin = this.plugin;
|
||||
|
||||
app.hideProgress();
|
||||
app.showMessage('success', "The plugin was saved successfully.");
|
||||
window.scrollTo( 0, 0 );
|
||||
|
||||
// copy active jobs to array
|
||||
var jobs = [];
|
||||
for (var id in app.activeJobs) {
|
||||
var job = app.activeJobs[id];
|
||||
if ((job.plugin == plugin.id) && !job.detached) jobs.push( job );
|
||||
}
|
||||
|
||||
// if the plugin was disabled and there are running jobs, ask user to abort them
|
||||
if (!plugin.enabled && jobs.length) {
|
||||
app.confirm( '<span style="color:red">Abort Jobs</span>', "There " + ((jobs.length != 1) ? 'are' : 'is') + " currently still " + jobs.length + " active " + pluralize('job', jobs.length) + " using the disabled plugin <b>"+plugin.title+"</b>. Do you want to abort " + ((jobs.length != 1) ? 'these' : 'it') + " now?", "Abort", function(result) {
|
||||
if (result) {
|
||||
app.showProgress( 1.0, "Aborting " + pluralize('Job', jobs.length) + "..." );
|
||||
app.api.post( 'app/abort_jobs', { plugin: plugin.id }, function(resp) {
|
||||
app.hideProgress();
|
||||
if (resp.count > 0) {
|
||||
app.showMessage('success', "The " + pluralize('job', resp.count) + " " + ((resp.count != 1) ? 'were' : 'was') + " aborted successfully.");
|
||||
}
|
||||
else {
|
||||
app.showMessage('warning', "No jobs were aborted. It is likely they completed while the dialog was up.");
|
||||
}
|
||||
} );
|
||||
} // clicked Abort
|
||||
} ); // app.confirm
|
||||
} // disabled + jobs
|
||||
},
|
||||
|
||||
get_plugin_edit_html: function() {
|
||||
// get html for editing a plugin (or creating a new one)
|
||||
var html = '';
|
||||
var plugin = this.plugin;
|
||||
|
||||
// Internal ID
|
||||
if (plugin.id && this.isAdmin()) {
|
||||
html += get_form_table_row( 'Plugin ID', '<div style="font-size:14px;">' + plugin.id + '</div>' );
|
||||
html += get_form_table_caption( "The internal Plugin ID used for API calls. This cannot be changed." );
|
||||
html += get_form_table_spacer();
|
||||
}
|
||||
|
||||
// plugin title
|
||||
html += get_form_table_row( 'Plugin Name', '<input type="text" id="fe_ep_title" size="35" value="'+escape_text_field_value(plugin.title)+'" spellcheck="false"/>' );
|
||||
html += get_form_table_caption( "Enter a name for the Plugin. Ideally it should be somewhat short, and Title Case." );
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// plugin enabled
|
||||
html += get_form_table_row( 'Active', '<input type="checkbox" id="fe_ep_enabled" value="1" ' + (plugin.enabled ? 'checked="checked"' : '') + '/><label for="fe_ep_enabled">Plugin Enabled</label>' );
|
||||
html += get_form_table_caption( "Select whether events using this Plugin should be enabled or disabled in the schedule." );
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// command
|
||||
html += get_form_table_row('Executable:', '<textarea id="fe_ep_command" style="width:550px; height:50px; resize:vertical;" spellcheck="false" onkeydown="return $P().stopEnter(this,event)">'+escape_text_field_value(plugin.command)+'</textarea>');
|
||||
html += get_form_table_caption(
|
||||
'Enter the filesystem path to your executable, including any command-line arguments.<br/>' +
|
||||
'Do not include any pipes or redirects -- for those, please use the <b>Shell Plugin</b>.'
|
||||
);
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// params editor
|
||||
html += get_form_table_row( 'Parameters:', '<div id="d_ep_params">' + this.get_plugin_params_html() + '</div>' );
|
||||
html += get_form_table_caption(
|
||||
'<div style="margin-top:5px;">Parameters are passed to your Plugin via JSON, and as environment variables.<br/>' +
|
||||
'For example, you can use this to customize the PATH variable, if your Plugin requires it.</div>'
|
||||
);
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// advanced options
|
||||
var adv_expanded = !!(plugin.cwd || plugin.uid);
|
||||
html += get_form_table_row( 'Advanced',
|
||||
'<div style="font-size:13px;'+(adv_expanded ? 'display:none;' : '')+'"><span class="link addme" onMouseUp="$P().expand_fieldset($(this))"><i class="fa fa-plus-square-o"> </i>Advanced Options</span></div>' +
|
||||
'<fieldset style="padding:10px 10px 0 10px; margin-bottom:5px;'+(adv_expanded ? '' : 'display:none;')+'"><legend class="link addme" onMouseUp="$P().collapse_fieldset($(this))"><i class="fa fa-minus-square-o"> </i>Advanced Options</legend>' +
|
||||
'<div class="plugin_params_label">Working Directory (CWD):</div>' +
|
||||
'<div class="plugin_params_content"><input type="text" id="fe_ep_cwd" size="50" value="'+escape_text_field_value(plugin.cwd)+'" placeholder="" spellcheck="false"/></div>' +
|
||||
|
||||
'<div class="plugin_params_label">Run as User (UID):</div>' +
|
||||
'<div class="plugin_params_content"><input type="text" id="fe_ep_uid" size="20" value="'+escape_text_field_value(plugin.uid)+'" placeholder="" spellcheck="false"/></div>' +
|
||||
|
||||
'</fieldset>'
|
||||
);
|
||||
html += get_form_table_caption(
|
||||
"Optionally enter a working directory path, and/or a custom UID for the Plugin.<br>" +
|
||||
"The UID may be either numerical or a string ('root', 'wheel', etc.)."
|
||||
);
|
||||
html += get_form_table_spacer();
|
||||
|
||||
return html;
|
||||
},
|
||||
|
||||
stopEnter: function(item, e) {
|
||||
// prevent user from hitting enter in textarea
|
||||
var c = e.which ? e.which : e.keyCode;
|
||||
if (c == 13) {
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
// setTimeout("document.getElementById('"+item.id+"').focus();",0);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
get_plugin_params_html: function() {
|
||||
// return HTML for editing plugin params
|
||||
var params = this.plugin.params;
|
||||
var html = '';
|
||||
var ctype_labels = this.ctype_labels;
|
||||
|
||||
var cols = ['Param ID', 'Label', 'Control Type', 'Description', 'Actions'];
|
||||
|
||||
html += '<table class="data_table" width="100%">';
|
||||
html += '<tr><th>' + cols.join('</th><th>').replace(/\s+/g, ' ') + '</th></tr>';
|
||||
for (var idx = 0, len = params.length; idx < len; idx++) {
|
||||
var param = params[idx];
|
||||
var actions = [
|
||||
'<span class="link" onMouseUp="$P().edit_plugin_param('+idx+')"><b>Edit</b></span>',
|
||||
'<span class="link" onMouseUp="$P().delete_plugin_param('+idx+')"><b>Delete</b></span>'
|
||||
];
|
||||
html += '<tr>';
|
||||
html += '<td><span class="link" style="font-family:monospace; font-weight:bold; white-space:nowrap;" onMouseUp="$P().edit_plugin_param('+idx+')"><i class="fa fa-cog"> </i>' + param.id + '</span></td>';
|
||||
// html += '<td><span class="link" style="font-weight:bold" onMouseUp="$P().edit_plugin_param('+idx+')">' + param.title + '</span></td>';
|
||||
if (param.title) html += '<td><b>“' + param.title + '”</b></td>';
|
||||
else html += '<td>(n/a)</td>';
|
||||
|
||||
html += '<td>' + ctype_labels[param.type] + '</td>';
|
||||
|
||||
var pairs = [];
|
||||
switch (param.type) {
|
||||
case 'text':
|
||||
pairs.push([ 'Size', param.size ]);
|
||||
if ('value' in param) pairs.push([ 'Default', '“' + param.value + '”' ]);
|
||||
break;
|
||||
|
||||
case 'textarea':
|
||||
pairs.push([ 'Rows', param.rows ]);
|
||||
break;
|
||||
|
||||
case 'checkbox':
|
||||
pairs.push([ 'Default', param.value ? 'Checked' : 'Unchecked' ]);
|
||||
break;
|
||||
|
||||
case 'hidden':
|
||||
pairs.push([ 'Value', '“' + param.value + '”' ]);
|
||||
break;
|
||||
|
||||
case 'select':
|
||||
pairs.push([ 'Items', '(' + param.items.join(', ') + ')' ]);
|
||||
if ('value' in param) pairs.push([ 'Default', '“' + param.value + '”' ]);
|
||||
break;
|
||||
}
|
||||
for (var idy = 0, ley = pairs.length; idy < ley; idy++) {
|
||||
pairs[idy] = '<b>' + pairs[idy][0] + ':</b> ' + pairs[idy][1];
|
||||
}
|
||||
html += '<td>' + pairs.join(', ') + '</td>';
|
||||
|
||||
html += '<td>' + actions.join(' | ') + '</td>';
|
||||
html += '</tr>';
|
||||
} // foreach param
|
||||
if (!params.length) {
|
||||
html += '<tr><td colspan="'+cols.length+'" align="center" style="padding-top:10px; padding-bottom:10px; font-weight:bold;">';
|
||||
html += 'No params found.';
|
||||
html += '</td></tr>';
|
||||
}
|
||||
html += '</table>';
|
||||
|
||||
html += '<div class="button mini" style="width:110px; margin:10px 0 0 0" onMouseUp="$P().edit_plugin_param(-1)">Add Parameter...</div>';
|
||||
|
||||
return html;
|
||||
},
|
||||
|
||||
edit_plugin_param: function(idx) {
|
||||
// show dialog to edit or add plugin param
|
||||
var self = this;
|
||||
var param = (idx > -1) ? this.plugin.params[idx] : {
|
||||
id: "",
|
||||
type: "text",
|
||||
title: "",
|
||||
size: 20,
|
||||
value: ""
|
||||
};
|
||||
this.plugin_param = param;
|
||||
|
||||
var edit = (idx > -1) ? true : false;
|
||||
var html = '';
|
||||
|
||||
var ctype_labels = this.ctype_labels;
|
||||
var ctype_options = [
|
||||
['text', ctype_labels.text],
|
||||
['textarea', ctype_labels.textarea],
|
||||
['checkbox', ctype_labels.checkbox],
|
||||
['select', ctype_labels.select],
|
||||
['hidden', ctype_labels.hidden]
|
||||
];
|
||||
|
||||
html += '<table>' +
|
||||
get_form_table_row('Parameter ID:', '<input type="text" id="fe_epp_id" size="20" value="'+escape_text_field_value(param.id)+'"/>') +
|
||||
get_form_table_caption("Enter an ID for the parameter, which will be the JSON key.") +
|
||||
get_form_table_spacer() +
|
||||
get_form_table_row('Label:', '<input type="text" id="fe_epp_title" size="35" value="'+escape_text_field_value(param.title)+'"/>') +
|
||||
get_form_table_caption("Enter a label, which will be displayed next to the control.") +
|
||||
// get_form_table_spacer() +
|
||||
// get_form_table_row('Control Type:', '<select id="fe_epp_ctype" onChange="$P().change_plugin_control_type()">' + render_menu_options(ctype_options, param.type, false) + '</select>') +
|
||||
// get_form_table_caption("Select the type of control you want to display.") +
|
||||
'</table>';
|
||||
|
||||
html += '<fieldset style="margin-top:20px;">';
|
||||
html += '<legend><table cellspacing="0" cellpadding="0"><tr><td>Control Type: </td><td><select id="fe_epp_ctype" onChange="$P().change_plugin_control_type()">' + render_menu_options(ctype_options, param.type, false) + '</select></td></tr></table></legend>';
|
||||
html += '<div id="d_epp_editor" style="margin:5px 10px 5px 10px;">' + this.get_plugin_param_editor_html() + '</div>';
|
||||
html += '</fieldset>';
|
||||
|
||||
app.confirm( '<i class="fa fa-cog"> </i>' + (edit ? "Edit Parameter" : "Add Parameter"), html, edit ? "OK" : "Add", function(result) {
|
||||
app.clearError();
|
||||
|
||||
if (result) {
|
||||
param = self.get_plugin_param_values();
|
||||
if (!param) return;
|
||||
|
||||
if (edit) {
|
||||
// edit existing
|
||||
self.plugin.params[idx] = param;
|
||||
}
|
||||
else {
|
||||
// add new, check for unique id
|
||||
if (find_object(self.plugin.params, { id: param.id })) {
|
||||
return add.badField('fe_epp_id', "That parameter ID is already taken. Please enter a unique value.");
|
||||
}
|
||||
|
||||
self.plugin.params.push( param );
|
||||
}
|
||||
|
||||
Dialog.hide();
|
||||
|
||||
// refresh param list
|
||||
self.refresh_plugin_params();
|
||||
|
||||
} // user clicked add
|
||||
} ); // app.confirm
|
||||
|
||||
if (!edit) setTimeout( function() {
|
||||
$('#fe_epp_id').focus();
|
||||
}, 1 );
|
||||
},
|
||||
|
||||
get_plugin_param_editor_html: function() {
|
||||
// get html for editing one plugin param, new or edit
|
||||
var param = this.plugin_param;
|
||||
var html = '<table>';
|
||||
|
||||
switch (param.type) {
|
||||
case 'text':
|
||||
html += get_form_table_row('Size:', '<input type="text" id="fe_epp_text_size" size="5" value="'+escape_text_field_value(param.size)+'"/>');
|
||||
html += get_form_table_caption("Enter the size of the text field, in characters.");
|
||||
html += get_form_table_spacer('short transparent');
|
||||
html += get_form_table_row('Default Value:', '<input type="text" id="fe_epp_text_value" size="35" value="'+escape_text_field_value(param.value)+'" spellcheck="false"/>');
|
||||
html += get_form_table_caption("Enter the default value for the text field.");
|
||||
break;
|
||||
|
||||
case 'textarea':
|
||||
html += get_form_table_row('Rows:', '<input type="text" id="fe_epp_textarea_rows" size="5" value="'+escape_text_field_value(param.rows || 5)+'"/>');
|
||||
html += get_form_table_caption("Enter the number of visible rows to allocate for the text box.");
|
||||
html += get_form_table_spacer('short transparent');
|
||||
html += get_form_table_row('Default Text:', '<textarea id="fe_epp_textarea_value" style="width:99%; height:60px; resize:none;" spellcheck="false">'+escape_text_field_value(param.value)+'</textarea>');
|
||||
html += get_form_table_caption("Optionally enter default text for the text box.");
|
||||
break;
|
||||
|
||||
case 'checkbox':
|
||||
html += get_form_table_row('Default State:', '<select id="fe_epp_checkbox_value">' + render_menu_options([[0,'Unchecked'], [1,'Checked']], param.value, false) + '</select>');
|
||||
html += get_form_table_caption("Select whether the checkbox should be initially checked or unchecked.");
|
||||
break;
|
||||
|
||||
case 'hidden':
|
||||
html += get_form_table_row('Value:', '<input type="text" id="fe_epp_hidden_value" size="35" value="'+escape_text_field_value(param.value)+'" spellcheck="false"/>');
|
||||
html += get_form_table_caption("Enter the value for the hidden field.");
|
||||
break;
|
||||
|
||||
case 'select':
|
||||
html += get_form_table_row('Menu Items:', '<input type="text" id="fe_epp_select_items" size="35" value="'+escape_text_field_value(param.items ? param.items.join(', ') : '')+'" spellcheck="false"/>');
|
||||
html += get_form_table_caption("Enter a comma-separated list of items for the menu.");
|
||||
html += get_form_table_spacer('short transparent');
|
||||
html += get_form_table_row('Selected Item:', '<input type="text" id="fe_epp_select_value" size="20" value="'+escape_text_field_value(param.value)+'" spellcheck="false"/>');
|
||||
html += get_form_table_caption("Optionally enter an item to be selected by default.");
|
||||
break;
|
||||
} // switch type
|
||||
|
||||
html += '</table>';
|
||||
return html;
|
||||
},
|
||||
|
||||
get_plugin_param_values: function() {
|
||||
// build up new 'param' object based on edit form (gen'ed from get_plugin_edit_controls())
|
||||
var param = { type: this.plugin_param.type };
|
||||
|
||||
param.id = trim( $('#fe_epp_id').val() );
|
||||
if (!param.id) return app.badField('fe_epp_id', "Please enter an ID for the plugin parameter.");
|
||||
if (!param.id.match(/^\w+$/)) return app.badField('fe_epp_id', "The parameter ID needs to be alphanumeric.");
|
||||
|
||||
param.title = trim( $('#fe_epp_title').val() );
|
||||
if ((param.type != 'hidden') && !param.title) return app.badField('fe_epp_title', "Please enter a label for the plugin parameter.");
|
||||
|
||||
switch (param.type) {
|
||||
case 'text':
|
||||
param.size = trim( $('#fe_epp_text_size').val() );
|
||||
if (!param.size.match(/^\d+$/)) return app.badField('fe_epp_text_size', "Please enter a size for the text field.");
|
||||
param.size = parseInt( param.size );
|
||||
if (!param.size) return app.badField('fe_epp_text_size', "Please enter a size for the text field.");
|
||||
if (param.size > 40) return app.badField('fe_epp_text_size', "The text field size needs to be between 1 and 40 characters.");
|
||||
param.value = trim( $('#fe_epp_text_value').val() );
|
||||
break;
|
||||
|
||||
case 'textarea':
|
||||
param.rows = trim( $('#fe_epp_textarea_rows').val() );
|
||||
if (!param.rows.match(/^\d+$/)) return app.badField('fe_epp_textarea_rows', "Please enter a number of rows for the text box.");
|
||||
param.rows = parseInt( param.rows );
|
||||
if (!param.rows) return app.badField('fe_epp_textarea_rows', "Please enter a number of rows for the text box.");
|
||||
if (param.rows > 50) return app.badField('fe_epp_textarea_rows', "The text box rows needs to be between 1 and 50.");
|
||||
param.value = trim( $('#fe_epp_textarea_value').val() );
|
||||
break;
|
||||
|
||||
case 'checkbox':
|
||||
param.value = parseInt( trim( $('#fe_epp_checkbox_value').val() ) );
|
||||
break;
|
||||
|
||||
case 'hidden':
|
||||
param.value = trim( $('#fe_epp_hidden_value').val() );
|
||||
break;
|
||||
|
||||
case 'select':
|
||||
if (!$('#fe_epp_select_items').val().match(/\S/)) return app.badField('fe_epp_select_items', "Please enter a comma-separated list of items for the menu.");
|
||||
param.items = trim( $('#fe_epp_select_items').val() ).split(/\,\s*/);
|
||||
param.value = trim( $('#fe_epp_select_value').val() );
|
||||
if (param.value && !find_in_array(param.items, param.value)) return app.badField('fe_epp_select_value', "The default value you entered was not found in the list of menu items.");
|
||||
break;
|
||||
}
|
||||
|
||||
return param;
|
||||
},
|
||||
|
||||
change_plugin_control_type: function() {
|
||||
// change dialog to new control type
|
||||
// render, resize and reposition dialog
|
||||
var new_type = $('#fe_epp_ctype').val();
|
||||
this.plugin_param.type = new_type;
|
||||
|
||||
$('#d_epp_editor').html( this.get_plugin_param_editor_html() );
|
||||
|
||||
// Dialog.autoResize();
|
||||
},
|
||||
|
||||
delete_plugin_param: function(idx) {
|
||||
// delete selected plugin param, but do not save
|
||||
// don't prompt either, giving a UX hint that save did not occur
|
||||
this.plugin.params.splice( idx, 1 );
|
||||
this.refresh_plugin_params();
|
||||
},
|
||||
|
||||
refresh_plugin_params: function() {
|
||||
// redraw plugin param area after change
|
||||
$('#d_ep_params').html( this.get_plugin_params_html() );
|
||||
},
|
||||
|
||||
get_plugin_form_json: function() {
|
||||
// get plugin elements from form, used for new or edit
|
||||
var plugin = this.plugin;
|
||||
|
||||
plugin.title = trim( $('#fe_ep_title').val() );
|
||||
if (!plugin.title) return app.badField('fe_ep_title', "Please enter a title for the Plugin.");
|
||||
|
||||
plugin.enabled = $('#fe_ep_enabled').is(':checked') ? 1 : 0;
|
||||
|
||||
plugin.command = trim( $('#fe_ep_command').val() );
|
||||
if (!plugin.command) return app.badField('fe_ep_command', "Please enter a filesystem path to the executable command for the Plugin.");
|
||||
if (plugin.command.match(/[\n\r]/)) return app.badField('fe_ep_command', "You must not include any newlines (EOLs) in your command. Please consider using the built-in Shell Plugin.");
|
||||
|
||||
plugin.cwd = trim( $('#fe_ep_cwd').val() );
|
||||
plugin.uid = trim( $('#fe_ep_uid').val() );
|
||||
|
||||
if (plugin.uid.match(/^\d+$/)) plugin.uid = parseInt( plugin.uid );
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
});
|
391
workspaces/base-workspace/Cronicle-0.8.61/htdocs/js/pages/admin/Servers.js
Executable file
|
@ -0,0 +1,391 @@
|
|||
// Cronicle Admin Page -- Servers
|
||||
|
||||
Class.add( Page.Admin, {
|
||||
|
||||
gosub_servers: function(args) {
|
||||
// show server list, server groups
|
||||
this.div.removeClass('loading');
|
||||
app.setWindowTitle( "Servers" );
|
||||
|
||||
var size = get_inner_window_size();
|
||||
var col_width = Math.floor( ((size.width * 0.9) + 400) / 9 );
|
||||
|
||||
var html = '';
|
||||
|
||||
html += this.getSidebarTabs( 'servers',
|
||||
[
|
||||
['activity', "Activity Log"],
|
||||
['api_keys', "API Keys"],
|
||||
['categories', "Categories"],
|
||||
['plugins', "Plugins"],
|
||||
['servers', "Servers"],
|
||||
['users', "Users"]
|
||||
]
|
||||
);
|
||||
|
||||
html += '<div style="padding:20px 20px 30px 20px">';
|
||||
|
||||
// Active Server Cluster
|
||||
|
||||
var cols = ['Hostname', 'IP Address', 'Groups', 'Status', 'Active Jobs', 'Uptime', 'CPU', 'Mem', 'Actions'];
|
||||
|
||||
html += '<div class="subtitle">';
|
||||
html += 'Server Cluster';
|
||||
// html += '<div class="clear"></div>';
|
||||
html += '</div>';
|
||||
|
||||
this.servers = [];
|
||||
var hostnames = hash_keys_to_array( app.servers ).sort();
|
||||
for (var idx = 0, len = hostnames.length; idx < len; idx++) {
|
||||
this.servers.push( app.servers[ hostnames[idx] ] );
|
||||
}
|
||||
|
||||
// include nearby servers under main server list
|
||||
if (app.nearby) {
|
||||
var hostnames = hash_keys_to_array( app.nearby ).sort();
|
||||
for (var idx = 0, len = hostnames.length; idx < len; idx++) {
|
||||
var server = app.nearby[ hostnames[idx] ];
|
||||
if (!app.servers[server.hostname]) {
|
||||
server.nearby = 1;
|
||||
this.servers.push( server );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// render table
|
||||
var self = this;
|
||||
html += this.getBasicTable( this.servers, cols, 'server', function(server, idx) {
|
||||
|
||||
// render nearby servers differently
|
||||
if (server.nearby) {
|
||||
var tds = [
|
||||
'<div class="td_big" style="font-weight:normal"><div class="ellip" style="max-width:'+col_width+'px;"><i class="fa fa-eye"> </i>' + server.hostname.replace(/\.[\w\-]+\.\w+$/, '') + '</div></div>',
|
||||
(server.ip || 'n/a').replace(/^\:\:ffff\:(\d+\.\d+\.\d+\.\d+)$/, '$1'),
|
||||
'-', '(Nearby)', '-', '-', '-', '-',
|
||||
'<span class="link" onMouseUp="$P().add_server_from_list('+idx+')"><b>Add Server</b></span>'
|
||||
];
|
||||
tds.className = 'blue';
|
||||
return tds;
|
||||
} // nearby
|
||||
|
||||
var actions = [
|
||||
'<span class="link" onMouseUp="$P().restart_server('+idx+')"><b>Restart</b></span>',
|
||||
'<span class="link" onMouseUp="$P().shutdown_server('+idx+')"><b>Shutdown</b></span>'
|
||||
];
|
||||
if (server.disabled) actions = [];
|
||||
if (!server.master) {
|
||||
actions.push( '<span class="link" onMouseUp="$P().remove_server('+idx+')"><b>Remove</b></span>' );
|
||||
}
|
||||
|
||||
var group_names = [];
|
||||
var eligible = false;
|
||||
for (var idx = 0, len = app.server_groups.length; idx < len; idx++) {
|
||||
var group = app.server_groups[idx];
|
||||
var regexp = new RegExp( group.regexp, "i" );
|
||||
if (server.hostname.match(regexp)) {
|
||||
group_names.push( group.title );
|
||||
if (group.master) eligible = true;
|
||||
}
|
||||
}
|
||||
|
||||
var jobs = find_objects( app.activeJobs, { hostname: server.hostname } );
|
||||
var num_jobs = jobs.length;
|
||||
|
||||
var cpu = 0;
|
||||
var mem = 0;
|
||||
if (server.data && server.data.cpu) cpu += server.data.cpu;
|
||||
if (server.data && server.data.mem) mem += server.data.mem;
|
||||
for (idx = 0, len = jobs.length; idx < len; idx++) {
|
||||
var job = jobs[idx];
|
||||
if (job.cpu && job.cpu.current) cpu += job.cpu.current;
|
||||
if (job.mem && job.mem.current) mem += job.mem.current;
|
||||
}
|
||||
|
||||
var tds = [
|
||||
'<div class="td_big">' + self.getNiceGroup(null, server.hostname, col_width) + '</div>',
|
||||
(server.ip || 'n/a').replace(/^\:\:ffff\:(\d+\.\d+\.\d+\.\d+)$/, '$1'),
|
||||
group_names.length ? group_names.join(', ') : '(None)',
|
||||
server.master ? '<span class="color_label green"><i class="fa fa-check"> </i>Primary</span>' : (eligible ? '<span class="color_label purple">Backup</span>' : '<span class="color_label blue">Worker</span>'),
|
||||
num_jobs ? commify( num_jobs ) : '(None)',
|
||||
get_text_from_seconds( server.uptime, true, true ).replace(/\bday\b/, 'days'),
|
||||
short_float(cpu) + '%',
|
||||
get_text_from_bytes(mem),
|
||||
actions.join(' | ')
|
||||
];
|
||||
|
||||
if (server.disabled) tds.className = 'disabled';
|
||||
|
||||
return tds;
|
||||
} );
|
||||
|
||||
html += '<div style="height:25px;"></div>';
|
||||
html += '<center><table><tr>';
|
||||
html += '<td><div class="button" style="width:130px;" onMouseUp="$P().add_server()"><i class="fa fa-plus-circle"> </i>Add Server...</div></td>';
|
||||
html += '</tr></table></center>';
|
||||
|
||||
html += '<div style="height:30px;"></div>';
|
||||
|
||||
// Server Groups
|
||||
|
||||
var col_width = Math.floor( ((size.width * 0.9) + 300) / 6 );
|
||||
|
||||
var cols = ['Title', 'Hostname Match', '# of Servers', '# of Events', 'Class', 'Actions'];
|
||||
|
||||
html += '<div class="subtitle">';
|
||||
html += 'Server Groups';
|
||||
// html += '<div class="clear"></div>';
|
||||
html += '</div>';
|
||||
|
||||
// sort by title ascending
|
||||
this.server_groups = app.server_groups.sort( function(a, b) {
|
||||
// return (b.title < a.title) ? 1 : -1;
|
||||
return a.title.toLowerCase().localeCompare( b.title.toLowerCase() );
|
||||
} );
|
||||
|
||||
// render table
|
||||
var self = this;
|
||||
html += this.getBasicTable( this.server_groups, cols, 'group', function(group, idx) {
|
||||
var actions = [
|
||||
'<span class="link" onMouseUp="$P().edit_group('+idx+')"><b>Edit</b></span>',
|
||||
'<span class="link" onMouseUp="$P().delete_group('+idx+')"><b>Delete</b></span>'
|
||||
];
|
||||
|
||||
var regexp = new RegExp( group.regexp, "i" );
|
||||
var num_servers = 0;
|
||||
for (var hostname in app.servers) {
|
||||
if (hostname.match(regexp)) num_servers++;
|
||||
}
|
||||
|
||||
var group_events = find_objects( app.schedule, { target: group.id } );
|
||||
var num_events = group_events.length;
|
||||
|
||||
return [
|
||||
'<div class="td_big" style="white-space:nowrap;"><span class="link" onMouseUp="$P().edit_group('+idx+')">' + self.getNiceGroup(group, null, col_width) + '</span></div>',
|
||||
'<div class="ellip" style="font-family:monospace; max-width:'+col_width+'px;">/' + encode_entities(group.regexp) + '/</div>',
|
||||
// group.description || '(No description)',
|
||||
num_servers ? commify( num_servers) : '(None)',
|
||||
num_events ? commify( num_events ) : '(None)',
|
||||
group.master ? '<b>Primary Eligible</b>' : 'Worker Only',
|
||||
actions.join(' | ')
|
||||
];
|
||||
} );
|
||||
|
||||
html += '<div style="height:25px;"></div>';
|
||||
html += '<center><table><tr>';
|
||||
html += '<td><div class="button" style="width:130px;" onMouseUp="$P().edit_group(-1)"><i class="fa fa-plus-circle"> </i>Add Group...</div></td>';
|
||||
html += '</tr></table></center>';
|
||||
|
||||
html += '</div>'; // padding
|
||||
html += '</div>'; // sidebar tabs
|
||||
|
||||
this.div.html( html );
|
||||
},
|
||||
|
||||
add_server_from_list: function(idx) {
|
||||
// add a server right away, from the nearby list
|
||||
var server = this.servers[idx];
|
||||
|
||||
app.showProgress( 1.0, "Adding server..." );
|
||||
app.api.post( 'app/add_server', { hostname: server.ip || server.hostname }, function(resp) {
|
||||
app.hideProgress();
|
||||
app.showMessage('success', "Server was added successfully.");
|
||||
// self['gosub_servers'](self.args);
|
||||
} ); // api.post
|
||||
},
|
||||
|
||||
add_server: function() {
|
||||
// show dialog allowing user to enter an arbitrary hostname to add
|
||||
var html = '';
|
||||
|
||||
// html += '<div style="font-size:12px; color:#777; margin-bottom:15px;">Typically, servers should automatically add themselves to the cluster, if they are within UDP broadcast range (i.e. on the same LAN). You should only need to manually add a server in special circumstances, e.g. if it is remotely hosted in another datacenter or network.</div>';
|
||||
|
||||
// html += '<div style="font-size:12px; color:#777; margin-bottom:20px;">Note that the new server cannot already be a master server, nor part of another '+app.name+' server cluster, and the current master server must be able to reach it.</div>';
|
||||
|
||||
html += '<center><table>' +
|
||||
// get_form_table_spacer() +
|
||||
get_form_table_row('Hostname or IP:', '<input type="text" id="fe_as_hostname" style="width:280px" value="" spellcheck="false"/>') +
|
||||
get_form_table_caption("Enter the hostname or IP of the server you want to add.") +
|
||||
'</table></center>';
|
||||
|
||||
app.confirm( '<i class="mdi mdi-desktop-tower mdi-lg"> </i>Add Server', html, "Add Server", function(result) {
|
||||
app.clearError();
|
||||
|
||||
if (result) {
|
||||
var hostname = $('#fe_as_hostname').val().toLowerCase();
|
||||
if (!hostname) return app.badField('fe_as_hostname', "Please enter a server hostname or IP address.");
|
||||
if (!hostname.match(/^[\w\-\.]+$/)) return app.badField('fe_as_hostname', "Please enter a valid server hostname or IP address.");
|
||||
if (app.servers[hostname]) return app.badField('fe_as_hostname', "That server is already in the cluster.");
|
||||
Dialog.hide();
|
||||
|
||||
app.showProgress( 1.0, "Adding server..." );
|
||||
app.api.post( 'app/add_server', { hostname: hostname }, function(resp) {
|
||||
app.hideProgress();
|
||||
app.showMessage('success', "Server was added successfully.");
|
||||
// self['gosub_servers'](self.args);
|
||||
} ); // api.post
|
||||
} // user clicked add
|
||||
} ); // app.confirm
|
||||
|
||||
setTimeout( function() {
|
||||
$('#fe_as_hostname').focus();
|
||||
}, 1 );
|
||||
},
|
||||
|
||||
remove_server: function(idx) {
|
||||
// remove manual server after user confirmation
|
||||
var server = this.servers[idx];
|
||||
|
||||
var jobs = find_objects( app.activeJobs, { hostname: server.hostname } );
|
||||
if (jobs.length) return app.doError("Sorry, you cannot remove a server that has active jobs running on it.");
|
||||
|
||||
// proceed with remove
|
||||
var self = this;
|
||||
app.confirm( '<span style="color:red">Remove Server</span>', "Are you sure you want to remove the server <b>"+server.hostname+"</b>?", "Remove", function(result) {
|
||||
if (result) {
|
||||
app.showProgress( 1.0, "Removing server..." );
|
||||
app.api.post( 'app/remove_server', server, function(resp) {
|
||||
app.hideProgress();
|
||||
app.showMessage('success', "Server was removed successfully.");
|
||||
// self.gosub_servers(self.args);
|
||||
} );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
edit_group: function(idx) {
|
||||
// edit group (-1 == new group)
|
||||
var self = this;
|
||||
var group = (idx > -1) ? this.server_groups[idx] : {
|
||||
title: "",
|
||||
regexp: "",
|
||||
master: 0
|
||||
};
|
||||
var edit = (idx > -1) ? true : false;
|
||||
var html = '';
|
||||
|
||||
html += '<table>';
|
||||
|
||||
// Internal ID
|
||||
if (edit && this.isAdmin()) {
|
||||
html += get_form_table_row( 'Group ID', '<div style="font-size:14px;">' + group.id + '</div>' );
|
||||
html += get_form_table_caption( "The internal Group ID used for API calls. This cannot be changed." );
|
||||
html += get_form_table_spacer();
|
||||
}
|
||||
|
||||
html +=
|
||||
get_form_table_row('Group Title:', '<input type="text" id="fe_eg_title" size="25" value="'+escape_text_field_value(group.title)+'"/>') +
|
||||
get_form_table_caption("Enter a title for the server group, short and sweet.") +
|
||||
get_form_table_spacer() +
|
||||
get_form_table_row('Hostname Match:', '<input type="text" id="fe_eg_regexp" size="30" style="font-family:monospace; font-size:13px;" value="'+escape_text_field_value(group.regexp)+'" spellcheck="false"/>') +
|
||||
get_form_table_caption("Enter a regular expression to auto-assign servers to this group by their hostnames, e.g. \"^mtx\\d+\\.\".") +
|
||||
get_form_table_spacer() +
|
||||
get_form_table_row('Server Class:', '<select id="fe_eg_master">' + render_menu_options([ [1,'Primary Eligible'], [0,'Worker Only'] ], group.master, false) + '</select>') +
|
||||
get_form_table_caption("Select whether servers in the group are eligible to become the primary server, or run as workers only.") +
|
||||
'</table>';
|
||||
|
||||
app.confirm( '<i class="mdi mdi-server-network"> </i>' + (edit ? "Edit Server Group" : "Add Server Group"), html, edit ? "Save Changes" : "Add Group", function(result) {
|
||||
app.clearError();
|
||||
|
||||
if (result) {
|
||||
group.title = $('#fe_eg_title').val();
|
||||
if (!group.title) return app.badField('fe_eg_title', "Please enter a title for the server group.");
|
||||
group.regexp = $('#fe_eg_regexp').val().replace(/^\/(.+)\/$/, '$1');
|
||||
if (!group.regexp) return app.badField('fe_eg_regexp', "Please enter a regular expression for the server group.");
|
||||
|
||||
try { new RegExp(group.regexp); }
|
||||
catch(err) {
|
||||
return app.badField('fe_eg_regexp', "Invalid regular expression: " + err);
|
||||
}
|
||||
|
||||
group.master = parseInt( $('#fe_eg_master').val() );
|
||||
Dialog.hide();
|
||||
|
||||
// pro-tip: embed id in title as bracketed prefix
|
||||
if (!edit && group.title.match(/^\[(\w+)\]\s*(.+)$/)) {
|
||||
group.id = RegExp.$1;
|
||||
group.title = RegExp.$2;
|
||||
}
|
||||
|
||||
app.showProgress( 1.0, edit ? "Saving group..." : "Adding group..." );
|
||||
app.api.post( edit ? 'app/update_server_group' : 'app/create_server_group', group, function(resp) {
|
||||
app.hideProgress();
|
||||
app.showMessage('success', "Server group was " + (edit ? "saved" : "added") + " successfully.");
|
||||
// self['gosub_servers'](self.args);
|
||||
} ); // api.post
|
||||
} // user clicked add
|
||||
} ); // app.confirm
|
||||
|
||||
setTimeout( function() {
|
||||
if (!$('#fe_eg_title').val()) $('#fe_eg_title').focus();
|
||||
}, 1 );
|
||||
},
|
||||
|
||||
delete_group: function(idx) {
|
||||
// delete selected server group
|
||||
var group = this.server_groups[idx];
|
||||
|
||||
// make sure user isn't deleting final master group
|
||||
if (group.master) {
|
||||
var num_masters = 0;
|
||||
for (var idx = 0, len = this.server_groups.length; idx < len; idx++) {
|
||||
if (this.server_groups[idx].master) num_masters++;
|
||||
}
|
||||
if (num_masters == 1) {
|
||||
return app.doError("Sorry, you cannot delete the last Primary Eligible server group.");
|
||||
}
|
||||
}
|
||||
|
||||
// check for events first
|
||||
var group_events = find_objects( app.schedule, { target: group.id } );
|
||||
var num_events = group_events.length;
|
||||
if (num_events) return app.doError("Sorry, you cannot delete a group that has events assigned to it.");
|
||||
|
||||
// proceed with delete
|
||||
var self = this;
|
||||
app.confirm( '<span style="color:red">Delete Server Group</span>', "Are you sure you want to delete the server group <b>"+group.title+"</b>? There is no way to undo this action.", "Delete", function(result) {
|
||||
if (result) {
|
||||
app.showProgress( 1.0, "Deleting group..." );
|
||||
app.api.post( 'app/delete_server_group', group, function(resp) {
|
||||
app.hideProgress();
|
||||
app.showMessage('success', "Server group was deleted successfully.");
|
||||
// self.gosub_servers(self.args);
|
||||
} );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
restart_server: function(idx) {
|
||||
// restart server after confirmation
|
||||
var self = this;
|
||||
var server = this.servers[idx];
|
||||
|
||||
app.confirm( '<span style="color:red">Restart Server</span>', "Are you sure you want to restart the server <b>"+server.hostname+"</b>? All server jobs will be aborted.", "Restart", function(result) {
|
||||
if (result) {
|
||||
app.showProgress( 1.0, "Restarting server..." );
|
||||
app.api.post( 'app/restart_server', server, function(resp) {
|
||||
app.hideProgress();
|
||||
app.showMessage('success', "Server is being restarted in the background.");
|
||||
// self.gosub_servers(self.args);
|
||||
} );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
shutdown_server: function(idx) {
|
||||
// shutdown server after confirmation
|
||||
var self = this;
|
||||
var server = this.servers[idx];
|
||||
|
||||
app.confirm( '<span style="color:red">Shutdown Server</span>', "Are you sure you want to shutdown the server <b>"+server.hostname+"</b>? All server jobs will be aborted.", "Shutdown", function(result) {
|
||||
if (result) {
|
||||
app.showProgress( 1.0, "Shutting down server..." );
|
||||
app.api.post( 'app/shutdown_server', server, function(resp) {
|
||||
app.hideProgress();
|
||||
app.showMessage('success', "Server is being shut down in the background.");
|
||||
// self.gosub_servers(self.args);
|
||||
} );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
});
|
590
workspaces/base-workspace/Cronicle-0.8.61/htdocs/js/pages/admin/Users.js
Executable file
|
@ -0,0 +1,590 @@
|
|||
// Cronicle Admin Page -- Users
|
||||
|
||||
Class.add( Page.Admin, {
|
||||
|
||||
gosub_users: function(args) {
|
||||
// show user list
|
||||
app.setWindowTitle( "User List" );
|
||||
this.div.addClass('loading');
|
||||
if (!args.offset) args.offset = 0;
|
||||
if (!args.limit) args.limit = 25;
|
||||
app.api.post( 'user/admin_get_users', copy_object(args), this.receive_users.bind(this) );
|
||||
},
|
||||
|
||||
receive_users: function(resp) {
|
||||
// receive page of users from server, render it
|
||||
this.lastUsersResp = resp;
|
||||
|
||||
var html = '';
|
||||
this.div.removeClass('loading');
|
||||
|
||||
var size = get_inner_window_size();
|
||||
var col_width = Math.floor( ((size.width * 0.9) + 200) / 7 );
|
||||
|
||||
this.users = [];
|
||||
if (resp.rows) this.users = resp.rows;
|
||||
|
||||
html += this.getSidebarTabs( 'users',
|
||||
[
|
||||
['activity', "Activity Log"],
|
||||
['api_keys', "API Keys"],
|
||||
['categories', "Categories"],
|
||||
['plugins', "Plugins"],
|
||||
['servers', "Servers"],
|
||||
['users', "Users"]
|
||||
]
|
||||
);
|
||||
|
||||
var cols = ['Username', 'Full Name', 'Email Address', 'Status', 'Type', 'Created', 'Actions'];
|
||||
|
||||
// html += '<div style="padding:5px 15px 15px 15px;">';
|
||||
html += '<div style="padding:20px 20px 30px 20px">';
|
||||
|
||||
html += '<div class="subtitle">';
|
||||
html += 'User Accounts';
|
||||
// html += '<div class="subtitle_widget"><span class="link" onMouseUp="$P().refresh_user_list()"><b>Refresh</b></span></div>';
|
||||
html += '<div class="subtitle_widget"><i class="fa fa-search"> </i><input type="text" id="fe_ul_search" size="15" placeholder="Find username..." style="border:0px;"/></div>';
|
||||
html += '<div class="clear"></div>';
|
||||
html += '</div>';
|
||||
|
||||
var self = this;
|
||||
html += this.getPaginatedTable( resp, cols, 'user', function(user, idx) {
|
||||
var actions = [
|
||||
'<span class="link" onMouseUp="$P().edit_user('+idx+')"><b>Edit</b></span>',
|
||||
'<span class="link" onMouseUp="$P().delete_user('+idx+')"><b>Delete</b></span>'
|
||||
];
|
||||
return [
|
||||
'<div class="td_big">' + self.getNiceUsername(user, true, col_width) + '</div>',
|
||||
'<div class="ellip" style="max-width:'+col_width+'px;">' + encode_entities(user.full_name) + '</div>',
|
||||
'<div class="ellip" style="max-width:'+col_width+'px;"><a href="mailto:'+user.email+'">'+encode_entities(user.email)+'</a></div>',
|
||||
user.active ? '<span class="color_label green"><i class="fa fa-check"> </i>Active</span>' : '<span class="color_label red"><i class="fa fa-warning"> </i>Suspended</span>',
|
||||
user.privileges.admin ? '<span class="color_label purple"><i class="fa fa-lock"> </i>Admin</span>' : '<span class="color_label gray">Standard</span>',
|
||||
'<span title="'+get_nice_date_time(user.created, true)+'">'+get_nice_date(user.created, true)+'</span>',
|
||||
actions.join(' | ')
|
||||
];
|
||||
} );
|
||||
|
||||
html += '<div style="height:30px;"></div>';
|
||||
html += '<center><table><tr>';
|
||||
html += '<td><div class="button" style="width:130px;" onMouseUp="$P().edit_user(-1)"><i class="fa fa-user-plus"> </i>Add User...</div></td>';
|
||||
html += '</tr></table></center>';
|
||||
|
||||
html += '</div>'; // padding
|
||||
html += '</div>'; // sidebar tabs
|
||||
|
||||
this.div.html( html );
|
||||
|
||||
setTimeout( function() {
|
||||
$('#fe_ul_search').keypress( function(event) {
|
||||
if (event.keyCode == '13') { // enter key
|
||||
event.preventDefault();
|
||||
$P().do_user_search( $('#fe_ul_search').val() );
|
||||
}
|
||||
} )
|
||||
.blur( function() { app.hideMessage(250); } )
|
||||
.keydown( function() { app.hideMessage(); } );
|
||||
}, 1 );
|
||||
},
|
||||
|
||||
do_user_search: function(username) {
|
||||
// see if user exists, edit if so
|
||||
app.api.post( 'user/admin_get_user', { username: username },
|
||||
function(resp) {
|
||||
Nav.go('Admin?sub=edit_user&username=' + username);
|
||||
},
|
||||
function(resp) {
|
||||
app.doError("User not found: " + username, 10);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
edit_user: function(idx) {
|
||||
// jump to edit sub
|
||||
if (idx > -1) Nav.go( '#Admin?sub=edit_user&username=' + this.users[idx].username );
|
||||
else if (app.config.external_users) {
|
||||
app.doError("Users are managed by an external system, so you cannot add users from here.");
|
||||
}
|
||||
else Nav.go( '#Admin?sub=new_user' );
|
||||
},
|
||||
|
||||
delete_user: function(idx) {
|
||||
// delete user from search results
|
||||
this.user = this.users[idx];
|
||||
this.show_delete_account_dialog();
|
||||
},
|
||||
|
||||
gosub_new_user: function(args) {
|
||||
// create new user
|
||||
var html = '';
|
||||
app.setWindowTitle( "Add New User" );
|
||||
this.div.removeClass('loading');
|
||||
|
||||
html += this.getSidebarTabs( 'new_user',
|
||||
[
|
||||
['activity', "Activity Log"],
|
||||
['api_keys', "API Keys"],
|
||||
['categories', "Categories"],
|
||||
['plugins', "Plugins"],
|
||||
['servers', "Servers"],
|
||||
['users', "Users"],
|
||||
['new_user', "Add New User"]
|
||||
]
|
||||
);
|
||||
|
||||
html += '<div style="padding:20px;"><div class="subtitle">Add New User</div></div>';
|
||||
|
||||
html += '<div style="padding:0px 20px 50px 20px">';
|
||||
html += '<center><table style="margin:0;">';
|
||||
|
||||
this.user = {
|
||||
privileges: copy_object( config.default_privileges )
|
||||
};
|
||||
|
||||
html += this.get_user_edit_html();
|
||||
|
||||
// notify user
|
||||
html += get_form_table_row( 'Notify', '<input type="checkbox" id="fe_eu_send_email" value="1" checked="checked"/><label for="fe_eu_send_email">Send Welcome Email</label>' );
|
||||
html += get_form_table_caption( "Select notification options for the new user." );
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// buttons at bottom
|
||||
html += '<tr><td colspan="2" align="center">';
|
||||
html += '<div style="height:30px;"></div>';
|
||||
|
||||
html += '<table><tr>';
|
||||
html += '<td><div class="button" style="width:120px; font-weight:normal;" onMouseUp="$P().cancel_user_edit()">Cancel</div></td>';
|
||||
html += '<td width="50"> </td>';
|
||||
if (config.debug) {
|
||||
html += '<td><div class="button" style="width:120px; font-weight:normal;" onMouseUp="$P().populate_random_user()">Randomize...</div></td>';
|
||||
html += '<td width="50"> </td>';
|
||||
}
|
||||
html += '<td><div class="button" style="width:120px;" onMouseUp="$P().do_new_user()"><i class="fa fa-user-plus"> </i>Create User</div></td>';
|
||||
html += '</tr></table>';
|
||||
|
||||
html += '</td></tr>';
|
||||
|
||||
html += '</table></center>';
|
||||
html += '</div>'; // table wrapper div
|
||||
|
||||
html += '</div>'; // sidebar tabs
|
||||
|
||||
this.div.html( html );
|
||||
|
||||
setTimeout( function() {
|
||||
$('#fe_eu_username').focus();
|
||||
}, 1 );
|
||||
},
|
||||
|
||||
cancel_user_edit: function() {
|
||||
// cancel editing user and return to list
|
||||
Nav.go( 'Admin?sub=users' );
|
||||
},
|
||||
|
||||
populate_random_user: function() {
|
||||
// grab random user data (for testing only)
|
||||
var self = this;
|
||||
|
||||
$.ajax({
|
||||
url: 'http://api.randomuser.me/',
|
||||
dataType: 'json',
|
||||
success: function(data){
|
||||
// console.log(data);
|
||||
if (data.results && data.results[0] && data.results[0].user) {
|
||||
var user = data.results[0].user;
|
||||
$('#fe_eu_username').val( user.username );
|
||||
$('#fe_eu_email').val( user.email );
|
||||
$('#fe_eu_fullname').val( ucfirst(user.name.first) + ' ' + ucfirst(user.name.last) );
|
||||
$('#fe_eu_send_email').prop( 'checked', false );
|
||||
self.generate_password();
|
||||
self.checkUserExists('eu');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
do_new_user: function(force) {
|
||||
// create new user
|
||||
app.clearError();
|
||||
var user = this.get_user_form_json();
|
||||
if (!user) return; // error
|
||||
|
||||
if (!user.username.length) {
|
||||
return app.badField('#fe_eu_username', "Please enter a username for the new account.");
|
||||
}
|
||||
if (!user.username.match(/^[\w\-\.]+$/)) {
|
||||
return app.badField('#fe_eu_username', "Please make sure the username contains only alphanumerics, periods and dashes.");
|
||||
}
|
||||
if (!user.email.length) {
|
||||
return app.badField('#fe_eu_email', "Please enter an e-mail address where the user can be reached.");
|
||||
}
|
||||
if (!user.email.match(/^\S+\@\S+$/)) {
|
||||
return app.badField('#fe_eu_email', "The e-mail address you entered does not appear to be correct.");
|
||||
}
|
||||
if (!user.full_name.length) {
|
||||
return app.badField('#fe_eu_fullname', "Please enter the user's first and last names.");
|
||||
}
|
||||
if (!user.password.length) {
|
||||
return app.badField('#fe_eu_password', "Please enter a secure password to protect the account.");
|
||||
}
|
||||
|
||||
user.send_email = $('#fe_eu_send_email').is(':checked') ? 1 : 0;
|
||||
|
||||
this.user = user;
|
||||
|
||||
app.showProgress( 1.0, "Creating user..." );
|
||||
app.api.post( 'user/admin_create', user, this.new_user_finish.bind(this) );
|
||||
},
|
||||
|
||||
new_user_finish: function(resp) {
|
||||
// new user created successfully
|
||||
app.hideProgress();
|
||||
|
||||
Nav.go('Admin?sub=edit_user&username=' + this.user.username);
|
||||
|
||||
setTimeout( function() {
|
||||
app.showMessage('success', "The new user account was created successfully.");
|
||||
}, 150 );
|
||||
},
|
||||
|
||||
gosub_edit_user: function(args) {
|
||||
// edit user subpage
|
||||
this.div.addClass('loading');
|
||||
app.api.post( 'user/admin_get_user', { username: args.username }, this.receive_user.bind(this) );
|
||||
},
|
||||
|
||||
receive_user: function(resp) {
|
||||
// edit existing user
|
||||
var html = '';
|
||||
app.setWindowTitle( "Editing User \"" + (this.args.username) + "\"" );
|
||||
this.div.removeClass('loading');
|
||||
|
||||
html += this.getSidebarTabs( 'edit_user',
|
||||
[
|
||||
['activity', "Activity Log"],
|
||||
['api_keys', "API Keys"],
|
||||
['categories', "Categories"],
|
||||
['plugins', "Plugins"],
|
||||
['servers', "Servers"],
|
||||
['users', "Users"],
|
||||
['edit_user', "Edit User"]
|
||||
]
|
||||
);
|
||||
|
||||
html += '<div style="padding:20px;"><div class="subtitle">Editing User “' + (this.args.username) + '”</div></div>';
|
||||
|
||||
html += '<div style="padding:0px 20px 50px 20px">';
|
||||
html += '<center>';
|
||||
html += '<table style="margin:0;">';
|
||||
|
||||
this.user = resp.user;
|
||||
|
||||
html += this.get_user_edit_html();
|
||||
|
||||
html += '<tr><td colspan="2" align="center">';
|
||||
html += '<div style="height:30px;"></div>';
|
||||
|
||||
html += '<table><tr>';
|
||||
html += '<td><div class="button" style="width:130px; font-weight:normal;" onMouseUp="$P().cancel_user_edit()">Cancel</div></td>';
|
||||
html += '<td width="50"> </td>';
|
||||
html += '<td><div class="button" style="width:130px; font-weight:normal;" onMouseUp="$P().show_delete_account_dialog()">Delete Account...</div></td>';
|
||||
html += '<td width="50"> </td>';
|
||||
html += '<td><div class="button" style="width:130px;" onMouseUp="$P().do_save_user()"><i class="fa fa-floppy-o"> </i>Save Changes</div></td>';
|
||||
html += '</tr></table>';
|
||||
|
||||
html += '</td></tr>';
|
||||
|
||||
html += '</table>';
|
||||
html += '</center>';
|
||||
html += '</div>'; // table wrapper div
|
||||
|
||||
html += '</div>'; // sidebar tabs
|
||||
|
||||
this.div.html( html );
|
||||
|
||||
setTimeout( function() {
|
||||
$('#fe_eu_username').attr('disabled', true);
|
||||
|
||||
if (app.config.external_users) {
|
||||
app.showMessage('warning', "Users are managed by an external system, so making changes here may have little effect.");
|
||||
// self.div.find('input').prop('disabled', true);
|
||||
}
|
||||
}, 1 );
|
||||
},
|
||||
|
||||
do_save_user: function() {
|
||||
// create new user
|
||||
app.clearError();
|
||||
var user = this.get_user_form_json();
|
||||
if (!user) return; // error
|
||||
|
||||
// if changing password, give server a hint
|
||||
if (user.password) {
|
||||
user.new_password = user.password;
|
||||
delete user.password;
|
||||
}
|
||||
|
||||
this.user = user;
|
||||
|
||||
app.showProgress( 1.0, "Saving user account..." );
|
||||
app.api.post( 'user/admin_update', user, this.save_user_finish.bind(this) );
|
||||
},
|
||||
|
||||
save_user_finish: function(resp, tx) {
|
||||
// new user created successfully
|
||||
app.hideProgress();
|
||||
app.showMessage('success', "The user was saved successfully.");
|
||||
window.scrollTo( 0, 0 );
|
||||
|
||||
// if we edited ourself, update header
|
||||
if (this.args.username == app.username) {
|
||||
app.user = resp.user;
|
||||
app.updateHeaderInfo();
|
||||
}
|
||||
|
||||
$('#fe_eu_password').val('');
|
||||
},
|
||||
|
||||
show_delete_account_dialog: function() {
|
||||
// show dialog confirming account delete action
|
||||
var self = this;
|
||||
|
||||
var msg = "Are you sure you want to <b>permanently delete</b> the user account \""+this.user.username+"\"? There is no way to undo this action, and no way to recover the data.";
|
||||
|
||||
if (app.config.external_users) {
|
||||
msg = "Are you sure you want to delete the user account \""+this.user.username+"\"? Users are managed by an external system, so this will have little effect here.";
|
||||
// return app.doError("Users are managed by an external system, so you cannot make changes here.");
|
||||
}
|
||||
|
||||
app.confirm( '<span style="color:red">Delete Account</span>', msg, 'Delete', function(result) {
|
||||
if (result) {
|
||||
app.showProgress( 1.0, "Deleting Account..." );
|
||||
app.api.post( 'user/admin_delete', {
|
||||
username: self.user.username
|
||||
}, self.delete_user_finish.bind(self) );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
delete_user_finish: function(resp, tx) {
|
||||
// finished deleting, immediately log user out
|
||||
var self = this;
|
||||
app.hideProgress();
|
||||
|
||||
Nav.go('Admin?sub=users', 'force');
|
||||
|
||||
setTimeout( function() {
|
||||
app.showMessage('success', "The user account '"+self.user.username+"' was deleted successfully.");
|
||||
}, 150 );
|
||||
},
|
||||
|
||||
get_user_edit_html: function() {
|
||||
// get html for editing a user (or creating a new one)
|
||||
var html = '';
|
||||
var user = this.user;
|
||||
|
||||
// user id
|
||||
html += get_form_table_row( 'Username',
|
||||
'<table cellspacing="0" cellpadding="0"><tr>' +
|
||||
'<td><input type="text" id="fe_eu_username" size="20" style="font-size:14px;" value="'+escape_text_field_value(user.username)+'" spellcheck="false" onChange="$P().checkUserExists(\'eu\')"/></td>' +
|
||||
'<td><div id="d_eu_valid" style="margin-left:5px; font-weight:bold;"></div></td>' +
|
||||
'</tr></table>'
|
||||
);
|
||||
html += get_form_table_caption( "Enter the username which identifies this account. Once entered, it cannot be changed. " );
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// account status
|
||||
html += get_form_table_row( 'Account Status', '<select id="fe_eu_status">' + render_menu_options([[1,'Active'], [0,'Suspended']], user.active) + '</select>' );
|
||||
html += get_form_table_caption( "'Suspended' means that the account remains in the system, but the user cannot log in." );
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// full name
|
||||
html += get_form_table_row( 'Full Name', '<input type="text" id="fe_eu_fullname" size="30" value="'+escape_text_field_value(user.full_name)+'" spellcheck="false"/>' );
|
||||
html += get_form_table_caption( "User's first and last name. They will not be shared with anyone outside the server.");
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// email
|
||||
html += get_form_table_row( 'Email Address', '<input type="text" id="fe_eu_email" size="30" value="'+escape_text_field_value(user.email)+'" spellcheck="false"/>' );
|
||||
html += get_form_table_caption( "This can be used to recover the password if the user forgets. It will not be shared with anyone outside the server." );
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// password
|
||||
html += get_form_table_row( user.password ? 'Change Password' : 'Password', '<input type="text" id="fe_eu_password" size="20" value="" spellcheck="false"/> <span class="link addme" onMouseUp="$P().generate_password()">« Generate Random</span>' );
|
||||
html += get_form_table_caption( user.password ? "Optionally enter a new password here to reset it. Please make it secure." : "Enter a password for the account. Please make it secure." );
|
||||
html += get_form_table_spacer();
|
||||
|
||||
// privilege list
|
||||
var priv_html = '';
|
||||
var user_is_admin = !!user.privileges.admin;
|
||||
|
||||
for (var idx = 0, len = config.privilege_list.length; idx < len; idx++) {
|
||||
var priv = config.privilege_list[idx];
|
||||
var has_priv = !!user.privileges[ priv.id ];
|
||||
var priv_visible = (priv.id == 'admin') || !user_is_admin;
|
||||
var priv_class = (priv.id == 'admin') ? 'priv_group_admin' : 'priv_group_other';
|
||||
|
||||
priv_html += '<div class="'+priv_class+'" style="margin-top:4px; margin-bottom:4px; '+(priv_visible ? '' : 'display:none;')+'">';
|
||||
priv_html += '<input type="checkbox" id="fe_eu_priv_'+priv.id+'" value="1" ' +
|
||||
(has_priv ? 'checked="checked" ' : '') + ((priv.id == 'admin') ? 'onChange="$P().change_admin_checkbox()"' : '') + '>';
|
||||
priv_html += '<label for="fe_eu_priv_'+priv.id+'">'+priv.title+'</label>';
|
||||
priv_html += '</div>';
|
||||
}
|
||||
|
||||
// user can be limited to certain categories
|
||||
var priv = { id: "cat_limit", title: "Limit to Categories" };
|
||||
var has_priv = !!user.privileges[ priv.id ];
|
||||
var priv_visible = !user_is_admin;
|
||||
|
||||
priv_html += '<div class="priv_group_other" style="margin-top:4px; margin-bottom:4px; '+(priv_visible ? '' : 'display:none;')+'">';
|
||||
priv_html += '<input type="checkbox" id="fe_eu_priv_'+priv.id+'" value="1" ' +
|
||||
(has_priv ? 'checked="checked" ' : '') + 'onChange="$P().change_cat_checkbox()"' + '>';
|
||||
priv_html += '<label for="fe_eu_priv_'+priv.id+'">'+priv.title+'</label>';
|
||||
priv_html += '</div>';
|
||||
|
||||
priv_html += '<div class="priv_group_other">';
|
||||
|
||||
// sort by title ascending
|
||||
var categories = app.categories.sort( function(a, b) {
|
||||
// return (b.title < a.title) ? 1 : -1;
|
||||
return a.title.toLowerCase().localeCompare( b.title.toLowerCase() );
|
||||
} );
|
||||
|
||||
for (var idx = 0, len = categories.length; idx < len; idx++) {
|
||||
var cat = categories[idx];
|
||||
var priv = { id: 'cat_' + cat.id, title: cat.title };
|
||||
var has_priv = !!user.privileges[ priv.id ];
|
||||
var priv_visible = !!user.privileges.cat_limit;
|
||||
|
||||
priv_html += '<div class="priv_group_cat" style="margin-top:4px; margin-bottom:4px; margin-left:20px; '+(priv_visible ? '' : 'display:none;')+'">';
|
||||
priv_html += '<input type="checkbox" id="fe_eu_priv_'+priv.id+'" value="1" ' +
|
||||
(has_priv ? 'checked="checked" ' : '') + '>';
|
||||
priv_html += '<label for="fe_eu_priv_'+priv.id+'" style="font-weight:normal"><i class="fa fa-folder-open-o"> </i>'+priv.title+'</label>';
|
||||
priv_html += '</div>';
|
||||
}
|
||||
|
||||
priv_html += '</div>';
|
||||
|
||||
// user can be limited to certain server groups
|
||||
var priv = { id: "grp_limit", title: "Limit to Server Groups" };
|
||||
var has_priv = !!user.privileges[ priv.id ];
|
||||
var priv_visible = !user_is_admin;
|
||||
|
||||
priv_html += '<div class="priv_group_other" style="margin-top:4px; margin-bottom:4px; '+(priv_visible ? '' : 'display:none;')+'">';
|
||||
priv_html += '<input type="checkbox" id="fe_eu_priv_'+priv.id+'" value="1" ' +
|
||||
(has_priv ? 'checked="checked" ' : '') + 'onChange="$P().change_grp_checkbox()"' + '>';
|
||||
priv_html += '<label for="fe_eu_priv_'+priv.id+'">'+priv.title+'</label>';
|
||||
priv_html += '</div>';
|
||||
|
||||
priv_html += '<div class="priv_group_other">';
|
||||
|
||||
// sort by title ascending
|
||||
var groups = app.server_groups.sort( function(a, b) {
|
||||
// return (b.title < a.title) ? 1 : -1;
|
||||
return a.title.toLowerCase().localeCompare( b.title.toLowerCase() );
|
||||
} );
|
||||
|
||||
for (var idx = 0, len = groups.length; idx < len; idx++) {
|
||||
var group = groups[idx];
|
||||
var priv = { id: 'grp_' + group.id, title: group.title };
|
||||
var has_priv = !!user.privileges[ priv.id ];
|
||||
var priv_visible = !!user.privileges.grp_limit;
|
||||
|
||||
priv_html += '<div class="priv_group_grp" style="margin-top:4px; margin-bottom:4px; margin-left:20px; '+(priv_visible ? '' : 'display:none;')+'">';
|
||||
priv_html += '<input type="checkbox" id="fe_eu_priv_'+priv.id+'" value="1" ' +
|
||||
(has_priv ? 'checked="checked" ' : '') + '>';
|
||||
priv_html += '<label for="fe_eu_priv_'+priv.id+'" style="font-weight:normal"><i class="fa fa-folder-open-o"> </i>'+priv.title+'</label>';
|
||||
priv_html += '</div>';
|
||||
}
|
||||
|
||||
priv_html += '</div>';
|
||||
|
||||
html += get_form_table_row( 'Privileges', priv_html );
|
||||
html += get_form_table_caption( "Select which privileges the user account should have. Administrators have all privileges." );
|
||||
html += get_form_table_spacer();
|
||||
|
||||
return html;
|
||||
},
|
||||
|
||||
change_admin_checkbox: function() {
|
||||
// toggle admin checkbox
|
||||
var is_checked = $('#fe_eu_priv_admin').is(':checked');
|
||||
if (is_checked) $('div.priv_group_other').hide(250);
|
||||
else $('div.priv_group_other').show(250);
|
||||
},
|
||||
|
||||
change_cat_checkbox: function() {
|
||||
// toggle category limit checkbox
|
||||
var is_checked = $('#fe_eu_priv_cat_limit').is(':checked');
|
||||
if (is_checked) $('div.priv_group_cat').show(250);
|
||||
else $('div.priv_group_cat').hide(250);
|
||||
},
|
||||
|
||||
change_grp_checkbox: function() {
|
||||
// toggle server group limit checkbox
|
||||
var is_checked = $('#fe_eu_priv_grp_limit').is(':checked');
|
||||
if (is_checked) $('div.priv_group_grp').show(250);
|
||||
else $('div.priv_group_grp').hide(250);
|
||||
},
|
||||
|
||||
get_user_form_json: function() {
|
||||
// get user elements from form, used for new or edit
|
||||
var user = {
|
||||
username: trim($('#fe_eu_username').val().toLowerCase()),
|
||||
active: $('#fe_eu_status').val(),
|
||||
full_name: trim($('#fe_eu_fullname').val()),
|
||||
email: trim($('#fe_eu_email').val()),
|
||||
password: $('#fe_eu_password').val(),
|
||||
privileges: {}
|
||||
};
|
||||
|
||||
user.privileges.admin = $('#fe_eu_priv_admin').is(':checked') ? 1 : 0;
|
||||
|
||||
if (!user.privileges.admin) {
|
||||
for (var idx = 0, len = config.privilege_list.length; idx < len; idx++) {
|
||||
var priv = config.privilege_list[idx];
|
||||
user.privileges[ priv.id ] = $('#fe_eu_priv_'+priv.id).is(':checked') ? 1 : 0;
|
||||
}
|
||||
|
||||
// category limit privs
|
||||
user.privileges.cat_limit = $('#fe_eu_priv_cat_limit').is(':checked') ? 1 : 0;
|
||||
|
||||
if (user.privileges.cat_limit) {
|
||||
var num_cat_privs = 0;
|
||||
for (var idx = 0, len = app.categories.length; idx < len; idx++) {
|
||||
var cat = app.categories[idx];
|
||||
var priv = { id: 'cat_' + cat.id };
|
||||
if ($('#fe_eu_priv_'+priv.id).is(':checked')) {
|
||||
user.privileges[ priv.id ] = 1;
|
||||
num_cat_privs++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!num_cat_privs) return app.doError("Please select at least one category privilege.");
|
||||
} // cat limit
|
||||
|
||||
// server group limit privs
|
||||
user.privileges.grp_limit = $('#fe_eu_priv_grp_limit').is(':checked') ? 1 : 0;
|
||||
|
||||
if (user.privileges.grp_limit) {
|
||||
var num_grp_privs = 0;
|
||||
for (var idx = 0, len = app.server_groups.length; idx < len; idx++) {
|
||||
var grp = app.server_groups[idx];
|
||||
var priv = { id: 'grp_' + grp.id };
|
||||
if ($('#fe_eu_priv_'+priv.id).is(':checked')) {
|
||||
user.privileges[ priv.id ] = 1;
|
||||
num_grp_privs++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!num_grp_privs) return app.doError("Please select at least one server group privilege.");
|
||||
} // grp limit
|
||||
} // not admin
|
||||
|
||||
return user;
|
||||
},
|
||||
|
||||
generate_password: function() {
|
||||
// generate random password
|
||||
$('#fe_eu_password').val( b64_md5(get_unique_id()).substring(0, 8) );
|
||||
}
|
||||
|
||||
});
|
489
workspaces/base-workspace/Cronicle-0.8.61/lib/api.js
Executable file
|
@ -0,0 +1,489 @@
|
|||
// Cronicle API Layer
|
||||
// Copyright (c) 2015 Joseph Huckaby
|
||||
// Released under the MIT License
|
||||
|
||||
var fs = require('fs');
|
||||
var assert = require("assert");
|
||||
var async = require('async');
|
||||
|
||||
var Class = require("pixl-class");
|
||||
var Tools = require("pixl-tools");
|
||||
|
||||
module.exports = Class.create({
|
||||
|
||||
__mixins: [
|
||||
require('./api/config.js'),
|
||||
require('./api/category.js'),
|
||||
require('./api/group.js'),
|
||||
require('./api/plugin.js'),
|
||||
require('./api/event.js'),
|
||||
require('./api/job.js'),
|
||||
require('./api/admin.js'),
|
||||
require('./api/apikey.js')
|
||||
],
|
||||
|
||||
api_ping: function(args, callback) {
|
||||
// hello
|
||||
callback({ code: 0 });
|
||||
},
|
||||
|
||||
api_echo: function(args, callback) {
|
||||
// for testing: adds 1 second delay, echoes everything back
|
||||
setTimeout( function() {
|
||||
callback({
|
||||
code: 0,
|
||||
query: args.query || {},
|
||||
params: args.params || {},
|
||||
files: args.files || {}
|
||||
});
|
||||
}, 1000 );
|
||||
},
|
||||
|
||||
api_check_user_exists: function(args, callback) {
|
||||
// checks if username is taken (used for showing green checkmark on form)
|
||||
var self = this;
|
||||
var query = args.query;
|
||||
var path = 'users/' + this.usermgr.normalizeUsername(query.username);
|
||||
|
||||
if (!this.requireParams(query, {
|
||||
username: this.usermgr.usernameMatch
|
||||
}, callback)) return;
|
||||
|
||||
// do not cache this API response
|
||||
this.forceNoCacheResponse(args);
|
||||
|
||||
this.storage.get(path, function(err, user) {
|
||||
callback({ code: 0, user_exists: !!user });
|
||||
} );
|
||||
},
|
||||
|
||||
api_status: function(args, callback) {
|
||||
// simple status, used by monitoring tools
|
||||
var tick_age = 0;
|
||||
var now = Tools.timeNow();
|
||||
if (this.lastTick) tick_age = now - this.lastTick;
|
||||
|
||||
// do not cache this API response
|
||||
this.forceNoCacheResponse(args);
|
||||
|
||||
var data = {
|
||||
code: 0,
|
||||
version: this.server.__version,
|
||||
node: process.version,
|
||||
hostname: this.server.hostname,
|
||||
ip: this.server.ip,
|
||||
pid: process.pid,
|
||||
now: now,
|
||||
uptime: Math.floor( now - (this.server.started || now) ),
|
||||
last_tick: this.lastTick || now,
|
||||
tick_age: tick_age,
|
||||
cpu: process.cpuUsage(),
|
||||
mem: process.memoryUsage()
|
||||
};
|
||||
|
||||
callback(data);
|
||||
|
||||
// self-check: if tick_age is over 60 seconds, log a level 1 debug alert
|
||||
if (tick_age > 60) {
|
||||
var msg = "EMERGENCY: Tick age is over 60 seconds (" + Math.floor(tick_age) + "s) -- Server should be restarted immediately.";
|
||||
this.logDebug(1, msg, data);
|
||||
|
||||
// JH 2018-08-28 Commenting this out for now, because an unsecured API should not have the power to cause an internal restart.
|
||||
// This kind of thing should be handled by external monitoring tools.
|
||||
// this.restartLocalServer({ reason: msg });
|
||||
}
|
||||
},
|
||||
|
||||
forceNoCacheResponse: function(args) {
|
||||
// make sure this response isn't cached, ever
|
||||
args.response.setHeader( 'Cache-Control', 'no-cache, no-store, must-revalidate, proxy-revalidate' );
|
||||
args.response.setHeader( 'Expires', 'Thu, 01 Jan 1970 00:00:00 GMT' );
|
||||
},
|
||||
|
||||
getServerBaseAPIURL: function(hostname, ip) {
|
||||
// construct fully-qualified URL to API on specified hostname
|
||||
// use proper protocol and ports as needed
|
||||
var api_url = '';
|
||||
|
||||
if (ip && !this.server.config.get('server_comm_use_hostnames')) hostname = ip;
|
||||
|
||||
if (this.web.config.get('https') && this.web.config.get('https_force')) {
|
||||
api_url = 'https://' + hostname;
|
||||
if (this.web.config.get('https_port') != 443) api_url += ':' + this.web.config.get('https_port');
|
||||
}
|
||||
else {
|
||||
api_url = 'http://' + hostname;
|
||||
if (this.web.config.get('http_port') != 80) api_url += ':' + this.web.config.get('http_port');
|
||||
}
|
||||
api_url += this.api.config.get('base_uri');
|
||||
|
||||
return api_url;
|
||||
},
|
||||
|
||||
validateOptionalParams: function(params, rules, callback) {
|
||||
// vaildate optional params given rule set
|
||||
assert( arguments.length == 3, "Wrong number of arguments to validateOptionalParams" );
|
||||
|
||||
for (var key in rules) {
|
||||
if (key in params) {
|
||||
var rule = rules[key];
|
||||
var type_regexp = rule[0];
|
||||
var value_regexp = rule[1];
|
||||
var value = params[key];
|
||||
var type_value = typeof(value);
|
||||
|
||||
if (!type_value.match(type_regexp)) {
|
||||
this.doError('api', "Malformed parameter type: " + key + " (" + type_value + ")", callback);
|
||||
return false;
|
||||
}
|
||||
else if (!value.toString().match(value_regexp)) {
|
||||
this.doError('api', "Malformed parameter value: " + key, callback);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
requireValidEventData: function(event, callback) {
|
||||
// make sure params contains valid event data (optional params)
|
||||
// otherwise throw an API error and return false
|
||||
// used by create_event, update_event, run_event and update_job APIs
|
||||
var RE_TYPE_STRING = /^(string)$/,
|
||||
RE_TYPE_BOOL = /^(boolean|number)$/,
|
||||
RE_TYPE_NUM = /^(number)$/,
|
||||
RE_ALPHANUM = /^\w+$/,
|
||||
RE_POS_INT = /^\d+$/,
|
||||
RE_BOOL = /^(\d+|true|false)$/;
|
||||
|
||||
var rules = {
|
||||
algo: [RE_TYPE_STRING, RE_ALPHANUM],
|
||||
api_key: [RE_TYPE_STRING, RE_ALPHANUM],
|
||||
catch_up: [RE_TYPE_BOOL, RE_BOOL],
|
||||
category: [RE_TYPE_STRING, RE_ALPHANUM],
|
||||
chain: [RE_TYPE_STRING, /^\w*$/],
|
||||
chain_error: [RE_TYPE_STRING, /^\w*$/],
|
||||
cpu_limit: [RE_TYPE_NUM, RE_POS_INT],
|
||||
cpu_sustain: [RE_TYPE_NUM, RE_POS_INT],
|
||||
created: [RE_TYPE_NUM, RE_POS_INT],
|
||||
detached: [RE_TYPE_BOOL, RE_BOOL],
|
||||
enabled: [RE_TYPE_BOOL, RE_BOOL],
|
||||
id: [RE_TYPE_STRING, RE_ALPHANUM],
|
||||
log_max_size: [RE_TYPE_NUM, RE_POS_INT],
|
||||
max_children: [RE_TYPE_NUM, RE_POS_INT],
|
||||
memory_limit: [RE_TYPE_NUM, RE_POS_INT],
|
||||
memory_sustain: [RE_TYPE_NUM, RE_POS_INT],
|
||||
modified: [RE_TYPE_NUM, RE_POS_INT],
|
||||
multiplex: [RE_TYPE_BOOL, RE_BOOL],
|
||||
notes: [RE_TYPE_STRING, /.*/],
|
||||
notify_fail: [RE_TYPE_STRING, /.*/],
|
||||
notify_success: [RE_TYPE_STRING, /.*/],
|
||||
plugin: [RE_TYPE_STRING, RE_ALPHANUM],
|
||||
queue: [RE_TYPE_BOOL, RE_BOOL],
|
||||
queue_max: [RE_TYPE_NUM, RE_POS_INT],
|
||||
retries: [RE_TYPE_NUM, RE_POS_INT],
|
||||
retry_delay: [RE_TYPE_NUM, RE_POS_INT],
|
||||
stagger: [RE_TYPE_NUM, RE_POS_INT],
|
||||
target: [RE_TYPE_STRING, /^[\w\-\.]+$/],
|
||||
timeout: [RE_TYPE_NUM, RE_POS_INT],
|
||||
timezone: [RE_TYPE_STRING, /.*/],
|
||||
title: [RE_TYPE_STRING, /\S/],
|
||||
username: [RE_TYPE_STRING, /^[\w\-\.]+$/],
|
||||
web_hook: [RE_TYPE_STRING, /(^$|https?\:\/\/\S+)/i]
|
||||
};
|
||||
if (!this.validateOptionalParams(event, rules, callback)) return false;
|
||||
|
||||
// make sure title doesn't contain HTML metacharacters
|
||||
if (event.title && event.title.match(/[<>]/)) {
|
||||
this.doError('api', "Malformed title parameter: Cannot contain HTML metacharacters", callback);
|
||||
return false;
|
||||
}
|
||||
|
||||
// params
|
||||
if (("params" in event) && (typeof(event.params) != 'object')) {
|
||||
this.doError('api', "Malformed event parameter: params (must be object)", callback);
|
||||
return false;
|
||||
}
|
||||
|
||||
// timing (can be falsey, or object)
|
||||
if (event.timing) {
|
||||
if (typeof(event.timing) != 'object') {
|
||||
this.doError('api', "Malformed event parameter: timing (must be object)", callback);
|
||||
return false;
|
||||
}
|
||||
|
||||
// check timing keys, should all be arrays of ints
|
||||
var timing = event.timing;
|
||||
for (var key in timing) {
|
||||
if (!key.match(/^(years|months|days|weekdays|hours|minutes)$/)) {
|
||||
this.doError('api', "Unknown event timing parameter: " + key, callback);
|
||||
return false;
|
||||
}
|
||||
var values = timing[key];
|
||||
if (!Tools.isaArray(values)) {
|
||||
this.doError('api', "Malformed event timing parameter: " + key + " (must be array)", callback);
|
||||
return false;
|
||||
}
|
||||
for (var idx = 0, len = values.length; idx < len; idx++) {
|
||||
var value = values[idx];
|
||||
if (typeof(value) != 'number') {
|
||||
this.doError('api', "Malformed event timing parameter: " + key + " (must be array of numbers)", callback);
|
||||
return false;
|
||||
}
|
||||
if ((key == 'years') && (value < 1)) {
|
||||
this.doError('api', "Malformed event timing parameter: " + key + " (value out of range: " + value + ")", callback);
|
||||
return false;
|
||||
}
|
||||
if ((key == 'months') && ((value < 1) || (value > 12))) {
|
||||
this.doError('api', "Malformed event timing parameter: " + key + " (value out of range: " + value + ")", callback);
|
||||
return false;
|
||||
}
|
||||
if ((key == 'days') && ((value < 1) || (value > 31))) {
|
||||
this.doError('api', "Malformed event timing parameter: " + key + " (value out of range: " + value + ")", callback);
|
||||
return false;
|
||||
}
|
||||
if ((key == 'weekdays') && ((value < 0) || (value > 6))) {
|
||||
this.doError('api', "Malformed event timing parameter: " + key + " (value out of range: " + value + ")", callback);
|
||||
return false;
|
||||
}
|
||||
if ((key == 'hours') && ((value < 0) || (value > 23))) {
|
||||
this.doError('api', "Malformed event timing parameter: " + key + " (value out of range: " + value + ")", callback);
|
||||
return false;
|
||||
}
|
||||
if ((key == 'minutes') && ((value < 0) || (value > 59))) {
|
||||
this.doError('api', "Malformed event timing parameter: " + key + " (value out of range: " + value + ")", callback);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // timing
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
requireValidUser: function(session, user, callback) {
|
||||
// make sure user and session are valid
|
||||
// otherwise throw an API error and return false
|
||||
|
||||
if (session && (session.type == 'api')) {
|
||||
// session is simulated, created by API key
|
||||
if (!user) {
|
||||
return this.doError('api', "Invalid API Key: " + session.api_key, callback);
|
||||
}
|
||||
if (!user.active) {
|
||||
return this.doError('api', "API Key is disabled: " + session.api_key, callback);
|
||||
}
|
||||
return true;
|
||||
} // api key
|
||||
|
||||
if (!session) {
|
||||
return this.doError('session', "Session has expired or is invalid.", callback);
|
||||
}
|
||||
if (!user) {
|
||||
return this.doError('user', "User not found: " + session.username, callback);
|
||||
}
|
||||
if (!user.active) {
|
||||
return this.doError('user', "User account is disabled: " + session.username, callback);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
requireAdmin: function(session, user, callback) {
|
||||
// make sure user and session are valid, and user is an admin
|
||||
// otherwise throw an API error and return false
|
||||
if (!this.requireValidUser(session, user, callback)) return false;
|
||||
|
||||
if (session.type == 'api') {
|
||||
// API Keys cannot be admins
|
||||
return this.doError('api', "API Key cannot use administrator features", callback);
|
||||
}
|
||||
|
||||
if (!user.privileges.admin) {
|
||||
return this.doError('user', "User is not an administrator: " + session.username, callback);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
hasPrivilege: function(user, priv_id) {
|
||||
// return true if user has privilege, false otherwise
|
||||
if (user.privileges.admin) return true; // admins can do everything
|
||||
if (user.privileges[priv_id]) return true;
|
||||
return false;
|
||||
},
|
||||
|
||||
requirePrivilege: function(user, priv_id, callback) {
|
||||
// make sure user has the specified privilege
|
||||
// otherwise throw an API error and return false
|
||||
if (this.hasPrivilege(user, priv_id)) return true;
|
||||
|
||||
if (user.key) {
|
||||
return this.doError('api', "API Key ('"+user.title+"') does not have the required privileges to perform this action ("+priv_id+").", callback);
|
||||
}
|
||||
else {
|
||||
return this.doError('user', "User '"+user.username+"' does not have the required account privileges to perform this action ("+priv_id+").", callback);
|
||||
}
|
||||
},
|
||||
|
||||
requireCategoryPrivilege: function(user, cat_id, callback) {
|
||||
// make sure user has the specified category privilege
|
||||
// otherwise throw an API error and return false
|
||||
if (user.privileges.admin) return true; // admins can do everything
|
||||
if (!user.privileges.cat_limit) return true; // user is not limited to categories
|
||||
|
||||
var priv_id = 'cat_' + cat_id;
|
||||
return this.requirePrivilege(user, priv_id, callback);
|
||||
},
|
||||
|
||||
requireGroupPrivilege: function(args, user, grp_id, callback) {
|
||||
// make sure user has the specified server group privilege
|
||||
// otherwise throw an API error and return false
|
||||
if (user.privileges.admin) return true; // admins can do everything
|
||||
if (!user.privileges.grp_limit) return true; // user is not limited to groups
|
||||
|
||||
var priv_id = 'grp_' + grp_id;
|
||||
var result = this.hasPrivilege(user, priv_id);
|
||||
if (result) return true;
|
||||
|
||||
// user may have targeted an individual server, so find its groups
|
||||
if (!args.server_groups) return false; // no groups loaded? hmmm...
|
||||
var groups = args.server_groups.filter( function(group) {
|
||||
return grp_id.match( group.regexp );
|
||||
} );
|
||||
|
||||
// we just need one group to match, then the user has permission to target the server
|
||||
for (var idx = 0, len = groups.length; idx < len; idx++) {
|
||||
priv_id = 'grp_' + groups[idx].id;
|
||||
if (this.hasPrivilege(user, priv_id, callback)) return true;
|
||||
}
|
||||
|
||||
// user does not have group privilege
|
||||
if (user.key) {
|
||||
return this.doError('api', "API Key ('"+user.title+"') does not have the required privileges to perform this action ("+priv_id+").", callback);
|
||||
}
|
||||
else {
|
||||
return this.doError('user', "User '"+user.username+"' does not have the required account privileges to perform this action ("+priv_id+").", callback);
|
||||
}
|
||||
},
|
||||
|
||||
requireMaster: function(args, callback) {
|
||||
// make sure we are the master server
|
||||
// otherwise throw an API error and return false
|
||||
if (this.multi.master) return true;
|
||||
|
||||
var status = "200 OK";
|
||||
var headers = {};
|
||||
|
||||
if (this.multi.masterHostname) {
|
||||
// we know who master is, so let's give the client a hint
|
||||
status = "302 Found";
|
||||
|
||||
var url = '';
|
||||
if (this.web.config.get('https') && this.web.config.get('https_force')) {
|
||||
url = 'https://' + (this.server.config.get('server_comm_use_hostnames') ? this.multi.masterHostname : this.multi.masterIP);
|
||||
if (this.web.config.get('https_port') != 443) url += ':' + this.web.config.get('https_port');
|
||||
}
|
||||
else {
|
||||
url = 'http://' + (this.server.config.get('server_comm_use_hostnames') ? this.multi.masterHostname : this.multi.masterIP);
|
||||
if (this.web.config.get('http_port') != 80) url += ':' + this.web.config.get('http_port');
|
||||
}
|
||||
url += args.request.url;
|
||||
|
||||
headers['Location'] = url;
|
||||
}
|
||||
|
||||
var msg = "This API call can only be invoked on the master server.";
|
||||
// this.logError( 'master', msg );
|
||||
callback( { code: 'master', description: msg }, status, headers );
|
||||
return false;
|
||||
},
|
||||
|
||||
getClientInfo: function(args, params) {
|
||||
// proxy over to user module
|
||||
// var info = this.usermgr.getClientInfo(args, params);
|
||||
var info = null;
|
||||
if (params) info = Tools.copyHash(params, true);
|
||||
else info = {};
|
||||
|
||||
info.ip = args.ip;
|
||||
info.headers = args.request.headers;
|
||||
|
||||
// augment with our own additions
|
||||
if (args.admin_user) info.username = args.admin_user.username;
|
||||
else if (args.user) {
|
||||
if (args.user.key) {
|
||||
// API Key
|
||||
info.api_key = args.user.key;
|
||||
info.api_title = args.user.title;
|
||||
}
|
||||
else {
|
||||
info.username = args.user.username;
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
},
|
||||
|
||||
loadSession: function(args, callback) {
|
||||
// Load user session or validate API Key
|
||||
var self = this;
|
||||
var session_id = args.cookies['session_id'] || args.request.headers['x-session-id'] || args.params.session_id || args.query.session_id;
|
||||
|
||||
if (session_id) {
|
||||
this.storage.get('sessions/' + session_id, function(err, session) {
|
||||
if (err) return callback(err, null, null);
|
||||
|
||||
// also load user
|
||||
self.storage.get('users/' + self.usermgr.normalizeUsername(session.username), function(err, user) {
|
||||
if (err) return callback(err, null, null);
|
||||
|
||||
// set type to discern this from API Key sessions
|
||||
session.type = 'user';
|
||||
|
||||
// get session_id out of args.params, so it doesn't interfere with API calls
|
||||
delete args.params.session_id;
|
||||
|
||||
// pass both session and user to callback
|
||||
callback(null, session, user);
|
||||
} );
|
||||
} );
|
||||
return;
|
||||
}
|
||||
|
||||
// no session found, look for API Key
|
||||
var api_key = args.request.headers['x-api-key'] || args.params.api_key || args.query.api_key;
|
||||
if (!api_key) return callback( new Error("No Session ID or API Key could be found"), null, null );
|
||||
|
||||
this.storage.listFind( 'global/api_keys', { key: api_key }, function(err, item) {
|
||||
if (err) return callback(new Error("API Key is invalid: " + api_key), null, null);
|
||||
|
||||
// create simulated session and user objects
|
||||
var session = {
|
||||
type: 'api',
|
||||
api_key: api_key
|
||||
};
|
||||
var user = item;
|
||||
|
||||
// get api_key out of args.params, so it doesn't interfere with API calls
|
||||
delete args.params.api_key;
|
||||
|
||||
// pass both "session" and "user" to callback
|
||||
callback(null, session, user);
|
||||
} );
|
||||
return;
|
||||
},
|
||||
|
||||
requireParams: function(params, rules, callback) {
|
||||
// proxy over to user module
|
||||
assert( arguments.length == 3, "Wrong number of arguments to requireParams" );
|
||||
return this.usermgr.requireParams(params, rules, callback);
|
||||
},
|
||||
|
||||
doError: function(code, msg, callback) {
|
||||
// proxy over to user module
|
||||
assert( arguments.length == 3, "Wrong number of arguments to doError" );
|
||||
return this.usermgr.doError( code, msg, callback );
|
||||
}
|
||||
|
||||
});
|
346
workspaces/base-workspace/Cronicle-0.8.61/lib/api/admin.js
Executable file
|
@ -0,0 +1,346 @@
|
|||
// Cronicle API Layer - Administrative
|
||||
// Copyright (c) 2015 Joseph Huckaby
|
||||
// Released under the MIT License
|
||||
|
||||
var fs = require('fs');
|
||||
var assert = require("assert");
|
||||
var async = require('async');
|
||||
|
||||
var Class = require("pixl-class");
|
||||
var Tools = require("pixl-tools");
|
||||
|
||||
module.exports = Class.create({
|
||||
|
||||
//
|
||||
// Servers / Master Control
|
||||
//
|
||||
|
||||
api_check_add_server: function(args, callback) {
|
||||
// check if it is okay to manually add this server to a remote cluster
|
||||
// (This is a server-to-server API, sent from master to a potential remote slave)
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
master: /\S/,
|
||||
now: /^\d+$/,
|
||||
token: /\S/
|
||||
}, callback)) return;
|
||||
|
||||
if (params.token != Tools.digestHex( params.master + params.now + this.server.config.get('secret_key') )) {
|
||||
return this.doError('server', "Secret keys do not match. Please synchronize your config files.", callback);
|
||||
}
|
||||
if (this.multi.master) {
|
||||
return this.doError('server', "Server is already a master server, controlling its own cluster.", callback);
|
||||
}
|
||||
if (this.multi.masterHostname && (this.multi.masterHostname != params.master)) {
|
||||
return this.doError('server', "Server is already a member of a cluster (Primary: " + this.multi.masterHostname + ")", callback);
|
||||
}
|
||||
|
||||
callback({ code: 0, hostname: this.server.hostname, ip: this.server.ip });
|
||||
},
|
||||
|
||||
api_add_server: function(args, callback) {
|
||||
// add any arbitrary server to cluster (i.e. outside of UDP broadcast range)
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
hostname: /\S/
|
||||
}, callback)) return;
|
||||
|
||||
var hostname = params.hostname.toLowerCase();
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireAdmin(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
// make sure server isn't already added
|
||||
if (self.slaves[hostname]) {
|
||||
return self.doError('server', "Server is already in cluster: " + hostname, callback);
|
||||
}
|
||||
|
||||
// send HTTP request to server, to make sure we can reach it
|
||||
var api_url = self.getServerBaseAPIURL( hostname ) + '/app/check_add_server';
|
||||
var now = Tools.timeNow(true);
|
||||
var api_args = {
|
||||
master: self.server.hostname,
|
||||
now: now,
|
||||
token: Tools.digestHex( self.server.hostname + now + self.server.config.get('secret_key') )
|
||||
};
|
||||
self.logDebug(9, "Sending API request to remote server: " + api_url);
|
||||
|
||||
// send request
|
||||
self.request.json( api_url, api_args, { timeout: 8 * 1000 }, function(err, resp, data) {
|
||||
if (err) {
|
||||
return self.doError('server', "Failed to contact server: " + err.message, callback );
|
||||
}
|
||||
if (resp.statusCode != 200) {
|
||||
return self.doError('server', "Failed to contact server: " + hostname + ": HTTP " + resp.statusCode + " " + resp.statusMessage, callback);
|
||||
}
|
||||
if (data.code != 0) {
|
||||
return self.doError('server', "Failed to add server to cluster: " + hostname + ": " + data.description, callback);
|
||||
}
|
||||
|
||||
// replace user-entered hostname with one returned from server (check_add_server api response)
|
||||
// just in case user entered an IP, or some CNAME
|
||||
hostname = data.hostname;
|
||||
|
||||
// re-check this, for sanity
|
||||
if (self.slaves[hostname]) {
|
||||
return self.doError('server', "Server is already in cluster: " + hostname, callback);
|
||||
}
|
||||
|
||||
// one more sanity check, with the IP this time
|
||||
for (var key in self.slaves) {
|
||||
var slave = self.slaves[key];
|
||||
if (slave.ip == data.ip) {
|
||||
return self.doError('server', "Server is already in cluster: " + hostname + " (" + data.ip + ")", callback);
|
||||
}
|
||||
}
|
||||
|
||||
// okay to add
|
||||
var stub = { hostname: hostname, ip: data.ip };
|
||||
self.logDebug(4, "Adding remote slave server to cluster: " + hostname, stub);
|
||||
self.addServer(stub, args);
|
||||
|
||||
// add to global/servers list
|
||||
self.storage.listFind( 'global/servers', { hostname: hostname }, function(err, item) {
|
||||
if (item) {
|
||||
// server is already in list, just ignore and go
|
||||
return callback({ code: 0 });
|
||||
}
|
||||
|
||||
// okay to add
|
||||
self.storage.listPush( 'global/servers', stub, function(err) {
|
||||
if (err) {
|
||||
// should never happen
|
||||
self.logError('server', "Failed to add server to storage: " + hostname + ": " + err);
|
||||
}
|
||||
|
||||
// success
|
||||
callback({ code: 0 });
|
||||
} ); // listPush
|
||||
} ); // listFind
|
||||
} ); // http request
|
||||
} ); // load session
|
||||
},
|
||||
|
||||
api_remove_server: function(args, callback) {
|
||||
// remove any manually-added server from cluster
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
hostname: /\S/
|
||||
}, callback)) return;
|
||||
|
||||
var hostname = params.hostname.toLowerCase();
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireAdmin(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
// do not allow removal of current master
|
||||
if (hostname == self.server.hostname) {
|
||||
return self.doError('server', "Cannot remove current master server: " + hostname, callback);
|
||||
}
|
||||
|
||||
var slave = self.slaves[hostname];
|
||||
if (!slave) {
|
||||
return self.doError('server', "Server not found in cluster: " + hostname, callback);
|
||||
}
|
||||
|
||||
// Do not allow removing server if it has any active jobs
|
||||
var all_jobs = self.getAllActiveJobs(true);
|
||||
for (var key in all_jobs) {
|
||||
var job = all_jobs[key];
|
||||
if (job.hostname == hostname) {
|
||||
var err = "Still has running jobs";
|
||||
return self.doError('server', "Failed to remove server: " + err, callback);
|
||||
} // matches server
|
||||
} // foreach job
|
||||
|
||||
// okay to remove
|
||||
self.logDebug(4, "Removing remote slave server from cluster: " + hostname);
|
||||
self.removeServer({ hostname: hostname }, args);
|
||||
|
||||
// delete from global/servers list
|
||||
self.storage.listFindDelete( 'global/servers', { hostname: hostname }, function(err) {
|
||||
if (err) {
|
||||
// should never happen
|
||||
self.logError('server', "Failed to remove server from storage: " + hostname + ": " + err);
|
||||
}
|
||||
|
||||
// success
|
||||
callback({ code: 0 });
|
||||
} ); // listFindDelete
|
||||
} ); // load session
|
||||
},
|
||||
|
||||
api_restart_server: function(args, callback) {
|
||||
// restart any server in cluster
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
hostname: /\S/
|
||||
}, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireAdmin(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
self.logTransaction('server_restart', '', self.getClientInfo(args, params));
|
||||
self.logActivity('server_restart', params, args);
|
||||
|
||||
var reason = "User request by: " + user.username;
|
||||
|
||||
if (params.hostname == self.server.hostname) {
|
||||
// restart this server
|
||||
self.restartLocalServer({ reason: reason });
|
||||
callback({ code: 0 });
|
||||
}
|
||||
else {
|
||||
// restart another server in the cluster
|
||||
var slave = self.slaves[ params.hostname ];
|
||||
if (slave && slave.socket) {
|
||||
self.logDebug(6, "Sending remote restart command to: " + slave.hostname);
|
||||
slave.socket.emit( 'restart_server', { reason: reason } );
|
||||
callback({ code: 0 });
|
||||
}
|
||||
else {
|
||||
callback({ code: 1, description: "Could not locate server: " + params.hostname });
|
||||
}
|
||||
}
|
||||
|
||||
} );
|
||||
},
|
||||
|
||||
api_shutdown_server: function(args, callback) {
|
||||
// shutdown any server in cluster
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
hostname: /\S/
|
||||
}, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireAdmin(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
self.logTransaction('server_shutdown', '', self.getClientInfo(args, params));
|
||||
self.logActivity('server_shutdown', params, args);
|
||||
|
||||
var reason = "User request by: " + user.username;
|
||||
|
||||
if (params.hostname == self.server.hostname) {
|
||||
// shutdown this server
|
||||
self.shutdownLocalServer({ reason: reason });
|
||||
callback({ code: 0 });
|
||||
}
|
||||
else {
|
||||
// shutdown another server in the cluster
|
||||
var slave = self.slaves[ params.hostname ];
|
||||
if (slave && slave.socket) {
|
||||
self.logDebug(6, "Sending remote shutdown command to: " + slave.hostname);
|
||||
slave.socket.emit( 'shutdown_server', { reason: reason } );
|
||||
callback({ code: 0 });
|
||||
}
|
||||
else {
|
||||
callback({ code: 1, description: "Could not locate server: " + params.hostname });
|
||||
}
|
||||
}
|
||||
|
||||
} );
|
||||
},
|
||||
|
||||
api_update_master_state: function(args, callback) {
|
||||
// update master state (i.e. scheduler enabled)
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireValidUser(session, user, callback)) return;
|
||||
if (!self.requirePrivilege(user, "state_update", callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
// import params into state
|
||||
self.logDebug(4, "Updating master state:", params);
|
||||
self.logTransaction('state_update', '', self.getClientInfo(args, params));
|
||||
self.logActivity('state_update', params, args);
|
||||
|
||||
if (params.enabled) {
|
||||
// need to re-initialize schedule if being enabled
|
||||
var now = Tools.normalizeTime( Tools.timeNow(), { sec: 0 } );
|
||||
var cursors = self.state.cursors;
|
||||
|
||||
self.storage.listGet( 'global/schedule', 0, 0, function(err, items) {
|
||||
// got all schedule items
|
||||
for (var idx = 0, len = items.length; idx < len; idx++) {
|
||||
var item = items[idx];
|
||||
|
||||
// reset cursor to now if event is NOT set to catch up
|
||||
if (!item.catch_up) {
|
||||
cursors[ item.id ] = now;
|
||||
}
|
||||
} // foreach item
|
||||
|
||||
// now it's safe to enable
|
||||
Tools.mergeHashInto( self.state, params );
|
||||
self.authSocketEmit( 'update', { state: self.state } );
|
||||
} ); // loaded schedule
|
||||
} // params.enabled
|
||||
else {
|
||||
// not enabling scheduler, so merge right away
|
||||
Tools.mergeHashInto( self.state, params );
|
||||
self.authSocketEmit( 'update', { state: self.state } );
|
||||
}
|
||||
|
||||
callback({ code: 0 });
|
||||
} );
|
||||
},
|
||||
|
||||
api_get_activity: function(args, callback) {
|
||||
// get rows from activity log (with pagination)
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireAdmin(session, user, callback)) return;
|
||||
|
||||
self.storage.listGet( 'logs/activity', parseInt(params.offset || 0), parseInt(params.limit || 50), function(err, items, list) {
|
||||
if (err) {
|
||||
// no rows found, not an error for this API
|
||||
return callback({ code: 0, rows: [], list: { length: 0 } });
|
||||
}
|
||||
|
||||
// success, return rows and list header
|
||||
callback({ code: 0, rows: items, list: list });
|
||||
} ); // got data
|
||||
} ); // loaded session
|
||||
}
|
||||
|
||||
} );
|
190
workspaces/base-workspace/Cronicle-0.8.61/lib/api/apikey.js
Executable file
|
@ -0,0 +1,190 @@
|
|||
// Cronicle API Layer - API Keys
|
||||
// Copyright (c) 2015 Joseph Huckaby
|
||||
// Released under the MIT License
|
||||
|
||||
var fs = require('fs');
|
||||
var assert = require("assert");
|
||||
var async = require('async');
|
||||
|
||||
var Class = require("pixl-class");
|
||||
var Tools = require("pixl-tools");
|
||||
|
||||
module.exports = Class.create({
|
||||
|
||||
api_get_api_keys: function(args, callback) {
|
||||
// get list of all api_keys
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireAdmin(session, user, callback)) return;
|
||||
|
||||
self.storage.listGet( 'global/api_keys', 0, 0, function(err, items, list) {
|
||||
if (err) {
|
||||
// no keys found, not an error for this API
|
||||
return callback({ code: 0, rows: [], list: { length: 0 } });
|
||||
}
|
||||
|
||||
// success, return keys and list header
|
||||
callback({ code: 0, rows: items, list: list });
|
||||
} ); // got api_key list
|
||||
} ); // loaded session
|
||||
},
|
||||
|
||||
api_get_api_key: function(args, callback) {
|
||||
// get single API Key for editing
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireAdmin(session, user, callback)) return;
|
||||
|
||||
self.storage.listFind( 'global/api_keys', { id: params.id }, function(err, item) {
|
||||
if (err || !item) {
|
||||
return self.doError('api_key', "Failed to locate API Key: " + params.id, callback);
|
||||
}
|
||||
|
||||
// success, return key
|
||||
callback({ code: 0, api_key: item });
|
||||
} ); // got api_key
|
||||
} ); // loaded session
|
||||
},
|
||||
|
||||
api_create_api_key: function(args, callback) {
|
||||
// add new API Key
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
title: /\S/,
|
||||
key: /\S/
|
||||
}, callback)) return;
|
||||
|
||||
// make sure title doesn't contain HTML metacharacters
|
||||
if (params.title && params.title.match(/[<>]/)) {
|
||||
return this.doError('api', "Malformed title parameter: Cannot contain HTML metacharacters", callback);
|
||||
}
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireAdmin(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
params.id = self.getUniqueID('k');
|
||||
params.username = user.username;
|
||||
params.created = params.modified = Tools.timeNow(true);
|
||||
|
||||
if (!params.active) params.active = 1;
|
||||
if (!params.description) params.description = "";
|
||||
if (!params.privileges) params.privileges = {};
|
||||
|
||||
self.logDebug(6, "Creating new API Key: " + params.title, params);
|
||||
|
||||
self.storage.listUnshift( 'global/api_keys', params, function(err) {
|
||||
if (err) {
|
||||
return self.doError('api_key', "Failed to create api_key: " + err, callback);
|
||||
}
|
||||
|
||||
self.logDebug(6, "Successfully created api_key: " + params.title, params);
|
||||
self.logTransaction('apikey_create', params.title, self.getClientInfo(args, { api_key: params }));
|
||||
self.logActivity('apikey_create', { api_key: params }, args);
|
||||
|
||||
callback({ code: 0, id: params.id, key: params.key });
|
||||
|
||||
// broadcast update to all websocket clients
|
||||
self.authSocketEmit( 'update', { api_keys: {} } );
|
||||
} ); // list insert
|
||||
} ); // load session
|
||||
},
|
||||
|
||||
api_update_api_key: function(args, callback) {
|
||||
// update existing API Key
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
// make sure title doesn't contain HTML metacharacters
|
||||
if (params.title && params.title.match(/[<>]/)) {
|
||||
return this.doError('api', "Malformed title parameter: Cannot contain HTML metacharacters", callback);
|
||||
}
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireAdmin(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
params.modified = Tools.timeNow(true);
|
||||
|
||||
self.logDebug(6, "Updating API Key: " + params.id, params);
|
||||
|
||||
self.storage.listFindUpdate( 'global/api_keys', { id: params.id }, params, function(err, api_key) {
|
||||
if (err) {
|
||||
return self.doError('api_key', "Failed to update API Key: " + err, callback);
|
||||
}
|
||||
|
||||
self.logDebug(6, "Successfully updated API Key: " + api_key.title, params);
|
||||
self.logTransaction('apikey_update', api_key.title, self.getClientInfo(args, { api_key: api_key }));
|
||||
self.logActivity('apikey_update', { api_key: api_key }, args);
|
||||
|
||||
callback({ code: 0 });
|
||||
|
||||
// broadcast update to all websocket clients
|
||||
self.authSocketEmit( 'update', { api_keys: {} } );
|
||||
} );
|
||||
} );
|
||||
},
|
||||
|
||||
api_delete_api_key: function(args, callback) {
|
||||
// delete existing API Key
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireAdmin(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
self.logDebug(6, "Deleting API Key: " + params.id, params);
|
||||
|
||||
self.storage.listFindDelete( 'global/api_keys', { id: params.id }, function(err, api_key) {
|
||||
if (err) {
|
||||
return self.doError('api_key', "Failed to delete API Key: " + err, callback);
|
||||
}
|
||||
|
||||
self.logDebug(6, "Successfully deleted API Key: " + api_key.title, api_key);
|
||||
self.logTransaction('apikey_delete', api_key.title, self.getClientInfo(args, { api_key: api_key }));
|
||||
self.logActivity('apikey_delete', { api_key: api_key }, args);
|
||||
|
||||
callback({ code: 0 });
|
||||
|
||||
// broadcast update to all websocket clients
|
||||
self.authSocketEmit( 'update', { api_keys: {} } );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
} );
|
218
workspaces/base-workspace/Cronicle-0.8.61/lib/api/category.js
Executable file
|
@ -0,0 +1,218 @@
|
|||
// Cronicle API Layer - Categories
|
||||
// Copyright (c) 2015 Joseph Huckaby
|
||||
// Released under the MIT License
|
||||
|
||||
var fs = require('fs');
|
||||
var assert = require("assert");
|
||||
var async = require('async');
|
||||
|
||||
var Class = require("pixl-class");
|
||||
var Tools = require("pixl-tools");
|
||||
|
||||
module.exports = Class.create({
|
||||
|
||||
//
|
||||
// Categories:
|
||||
//
|
||||
|
||||
api_get_categories: function(args, callback) {
|
||||
// get list of categories (with pagination)
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireValidUser(session, user, callback)) return;
|
||||
|
||||
self.storage.listGet( 'global/categories', parseInt(params.offset || 0), parseInt(params.limit || 0), function(err, items, list) {
|
||||
if (err) {
|
||||
// no cats found, not an error for this API
|
||||
return callback({ code: 0, rows: [], list: { length: 0 } });
|
||||
}
|
||||
|
||||
// success, return cats and list header
|
||||
callback({ code: 0, rows: items, list: list });
|
||||
} ); // got category list
|
||||
} ); // loaded session
|
||||
},
|
||||
|
||||
api_create_category: function(args, callback) {
|
||||
// add new category
|
||||
var self = this;
|
||||
var cat = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(cat, {
|
||||
title: /\S/,
|
||||
max_children: /^\d+$/
|
||||
}, callback)) return;
|
||||
|
||||
// make sure title doesn't contain HTML metacharacters
|
||||
if (cat.title && cat.title.match(/[<>]/)) {
|
||||
return this.doError('api', "Malformed title parameter: Cannot contain HTML metacharacters", callback);
|
||||
}
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireAdmin(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
if (cat.id) cat.id = cat.id.toString().toLowerCase().replace(/\W+/g, '');
|
||||
if (!cat.id) cat.id = self.getUniqueID('c');
|
||||
|
||||
cat.created = cat.modified = Tools.timeNow(true);
|
||||
|
||||
if (user.key) {
|
||||
// API Key
|
||||
cat.api_key = user.key;
|
||||
}
|
||||
else {
|
||||
cat.username = user.username;
|
||||
}
|
||||
|
||||
self.logDebug(6, "Creating new category: " + cat.title, cat);
|
||||
|
||||
self.storage.listUnshift( 'global/categories', cat, function(err) {
|
||||
if (err) {
|
||||
return self.doError('category', "Failed to create category: " + err, callback);
|
||||
}
|
||||
|
||||
self.logDebug(6, "Successfully created category: " + cat.title, cat);
|
||||
self.logTransaction('cat_create', cat.title, self.getClientInfo(args, { cat: cat }));
|
||||
self.logActivity('cat_create', { cat: cat }, args);
|
||||
|
||||
callback({ code: 0, id: cat.id });
|
||||
|
||||
// broadcast update to all websocket clients
|
||||
self.updateClientData( 'categories' );
|
||||
} ); // list insert
|
||||
} ); // load session
|
||||
},
|
||||
|
||||
api_update_category: function(args, callback) {
|
||||
// update existing category
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
// make sure title doesn't contain HTML metacharacters
|
||||
if (params.title && params.title.match(/[<>]/)) {
|
||||
return this.doError('api', "Malformed title parameter: Cannot contain HTML metacharacters", callback);
|
||||
}
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireAdmin(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
self.storage.listFind( 'global/categories', { id: params.id }, function(err, cat) {
|
||||
if (err || !cat) {
|
||||
return self.doError('event', "Failed to locate category: " + params.id, callback);
|
||||
}
|
||||
|
||||
params.modified = Tools.timeNow(true);
|
||||
|
||||
self.logDebug(6, "Updating category: " + cat.title, params);
|
||||
|
||||
// pull abort flag out of event object, for use later
|
||||
var abort_jobs = 0;
|
||||
if (params.abort_jobs) {
|
||||
abort_jobs = params.abort_jobs;
|
||||
delete params.abort_jobs;
|
||||
}
|
||||
|
||||
self.storage.listFindUpdate( 'global/categories', { id: params.id }, params, function(err) {
|
||||
if (err) {
|
||||
return self.doError('category', "Failed to update category: " + err, callback);
|
||||
}
|
||||
|
||||
// merge params into cat, just so we have the full updated record
|
||||
for (var key in params) cat[key] = params[key];
|
||||
|
||||
self.logDebug(6, "Successfully updated category: " + cat.title, params);
|
||||
self.logTransaction('cat_update', cat.title, self.getClientInfo(args, { cat: params }));
|
||||
self.logActivity('cat_update', { cat: params }, args);
|
||||
|
||||
callback({ code: 0 });
|
||||
|
||||
// broadcast update to all websocket clients
|
||||
self.updateClientData( 'categories' );
|
||||
|
||||
// if cat is disabled, abort all applicable jobs
|
||||
if (!cat.enabled && abort_jobs) {
|
||||
var all_jobs = self.getAllActiveJobs(true);
|
||||
for (var key in all_jobs) {
|
||||
var job = all_jobs[key];
|
||||
if ((job.category == cat.id) && !job.detached) {
|
||||
var msg = "Category '" + cat.title + "' has been disabled.";
|
||||
self.logDebug(4, "Job " + job.id + " is being aborted: " + msg);
|
||||
self.abortJob({ id: job.id, reason: msg });
|
||||
} // matches cat
|
||||
} // foreach job
|
||||
} // cat disabled
|
||||
|
||||
// if cat is being enabled, force scheduler to re-tick the minute
|
||||
var dargs = Tools.getDateArgs( new Date() );
|
||||
if (params.enabled && !self.schedulerGraceTimer && !self.schedulerTicking && (dargs.sec != 59)) {
|
||||
self.schedulerMinuteTick( null, true );
|
||||
}
|
||||
|
||||
} ); // update cat
|
||||
} ); // find cat
|
||||
} ); // load session
|
||||
},
|
||||
|
||||
api_delete_category: function(args, callback) {
|
||||
// delete existing category
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireAdmin(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
// Do not allow deleting category if any matching events in schedule
|
||||
self.storage.listFind( 'global/schedule', { category: params.id }, function(err, item) {
|
||||
if (item) {
|
||||
return self.doError('plugin', "Failed to delete category: Still in use by one or more events.", callback);
|
||||
}
|
||||
|
||||
self.logDebug(6, "Deleting category: " + params.id);
|
||||
|
||||
self.storage.listFindDelete( 'global/categories', { id: params.id }, function(err, cat) {
|
||||
if (err) {
|
||||
return self.doError('category', "Failed to delete category: " + err, callback);
|
||||
}
|
||||
|
||||
self.logDebug(6, "Successfully deleted category: " + cat.title, cat);
|
||||
self.logTransaction('cat_delete', cat.title, self.getClientInfo(args, { cat: cat }));
|
||||
self.logActivity('cat_delete', { cat: cat }, args);
|
||||
|
||||
callback({ code: 0 });
|
||||
|
||||
// broadcast update to all websocket clients
|
||||
self.updateClientData( 'categories' );
|
||||
|
||||
} ); // listFindDelete (category)
|
||||
} ); // listFind (schedule)
|
||||
} ); // load session
|
||||
}
|
||||
|
||||
} );
|
57
workspaces/base-workspace/Cronicle-0.8.61/lib/api/config.js
Executable file
|
@ -0,0 +1,57 @@
|
|||
// Cronicle API Layer - Configuration
|
||||
// Copyright (c) 2015 Joseph Huckaby
|
||||
// Released under the MIT License
|
||||
|
||||
var fs = require('fs');
|
||||
var assert = require("assert");
|
||||
var async = require('async');
|
||||
|
||||
var Class = require("pixl-class");
|
||||
var Tools = require("pixl-tools");
|
||||
|
||||
module.exports = Class.create({
|
||||
|
||||
api_config: function(args, callback) {
|
||||
// send config to client
|
||||
var self = this;
|
||||
|
||||
// do not cache this API response
|
||||
this.forceNoCacheResponse(args);
|
||||
|
||||
// if there is no master server, this has to fail (will be polled for retries)
|
||||
if (!this.multi.masterHostname) {
|
||||
return callback({ code: 'master', description: "No master server found" });
|
||||
}
|
||||
|
||||
var resp = {
|
||||
code: 0,
|
||||
version: this.server.__version,
|
||||
config: Tools.mergeHashes( this.server.config.get('client'), {
|
||||
debug: this.server.debug ? 1 : 0,
|
||||
job_memory_max: this.server.config.get('job_memory_max'),
|
||||
base_api_uri: this.api.config.get('base_uri'),
|
||||
default_privileges: this.usermgr.config.get('default_privileges'),
|
||||
free_accounts: this.usermgr.config.get('free_accounts'),
|
||||
external_users: this.usermgr.config.get('external_user_api') ? 1 : 0,
|
||||
external_user_api: this.usermgr.config.get('external_user_api') || '',
|
||||
web_socket_use_hostnames: this.server.config.get('web_socket_use_hostnames') || 0,
|
||||
web_direct_connect: this.server.config.get('web_direct_connect') || 0,
|
||||
socket_io_transports: this.server.config.get('socket_io_transports') || 0
|
||||
} ),
|
||||
port: args.request.headers.ssl ? this.web.config.get('https_port') : this.web.config.get('http_port'),
|
||||
master_hostname: this.multi.masterHostname
|
||||
};
|
||||
|
||||
// if we're master, then return our ip for websocket connect
|
||||
if (this.multi.master) {
|
||||
resp.servers = {};
|
||||
resp.servers[ this.server.hostname ] = {
|
||||
hostname: this.server.hostname,
|
||||
ip: this.server.ip
|
||||
};
|
||||
}
|
||||
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
} );
|
547
workspaces/base-workspace/Cronicle-0.8.61/lib/api/event.js
Executable file
|
@ -0,0 +1,547 @@
|
|||
// Cronicle API Layer - Events
|
||||
// Copyright (c) 2015 Joseph Huckaby
|
||||
// Released under the MIT License
|
||||
|
||||
var fs = require('fs');
|
||||
var assert = require("assert");
|
||||
var async = require('async');
|
||||
|
||||
var Class = require("pixl-class");
|
||||
var Tools = require("pixl-tools");
|
||||
|
||||
module.exports = Class.create({
|
||||
|
||||
//
|
||||
// Events:
|
||||
//
|
||||
|
||||
api_get_schedule: function(args, callback) {
|
||||
// get list of scheduled events (with pagination)
|
||||
var self = this;
|
||||
var params = Tools.mergeHashes( args.params, args.query );
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireValidUser(session, user, callback)) return;
|
||||
|
||||
self.storage.listGet( 'global/schedule', parseInt(params.offset || 0), parseInt(params.limit || 0), function(err, items, list) {
|
||||
if (err) {
|
||||
// no items found, not an error for this API
|
||||
return callback({ code: 0, rows: [], list: { length: 0 } });
|
||||
}
|
||||
|
||||
// success, return keys and list header
|
||||
callback({ code: 0, rows: items, list: list });
|
||||
} ); // got event list
|
||||
} ); // loaded session
|
||||
},
|
||||
|
||||
api_get_event: function(args, callback) {
|
||||
// get single event for editing
|
||||
var self = this;
|
||||
var params = Tools.mergeHashes( args.params, args.query );
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
var criteria = {};
|
||||
if (params.id) criteria.id = params.id;
|
||||
else if (params.title) criteria.title = params.title;
|
||||
else return this.doError('event', "Failed to locate event: No criteria specified", callback);
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireValidUser(session, user, callback)) return;
|
||||
|
||||
self.storage.listFind( 'global/schedule', criteria, function(err, item) {
|
||||
if (err || !item) {
|
||||
return self.doError('event', "Failed to locate event: " + (params.id || params.title), callback);
|
||||
}
|
||||
|
||||
// success, return event
|
||||
var resp = { code: 0, event: item };
|
||||
if (item.queue) resp.queue = self.eventQueue[item.id] || 0;
|
||||
|
||||
// if event has any active jobs, include those as well
|
||||
var all_jobs = self.getAllActiveJobs(true);
|
||||
var event_jobs = [];
|
||||
for (var key in all_jobs) {
|
||||
var job = all_jobs[key];
|
||||
if (job.event == item.id) event_jobs.push(job);
|
||||
}
|
||||
if (event_jobs.length) resp.jobs = event_jobs;
|
||||
|
||||
callback(resp);
|
||||
} ); // got event
|
||||
} ); // loaded session
|
||||
},
|
||||
|
||||
api_create_event: function(args, callback) {
|
||||
// add new event
|
||||
var self = this;
|
||||
var event = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(event, {
|
||||
title: /\S/,
|
||||
enabled: /^(\d+|true|false)$/,
|
||||
category: /^\w+$/,
|
||||
target: /^[\w\-\.]+$/,
|
||||
plugin: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
// validate optional event data parameters
|
||||
if (!this.requireValidEventData(event, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireValidUser(session, user, callback)) return;
|
||||
if (!self.requirePrivilege(user, "create_events", callback)) return;
|
||||
if (!self.requireCategoryPrivilege(user, event.category, callback)) return;
|
||||
if (!self.requireGroupPrivilege(args, user, event.target, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
if (event.id) event.id = event.id.toString().toLowerCase().replace(/\W+/g, '');
|
||||
if (!event.id) event.id = self.getUniqueID('e');
|
||||
|
||||
event.created = event.modified = Tools.timeNow(true);
|
||||
|
||||
if (!event.max_children) event.max_children = 0;
|
||||
if (!event.timeout) event.timeout = 0;
|
||||
if (!event.timezone) event.timezone = self.tz;
|
||||
if (!event.params) event.params = {};
|
||||
|
||||
if (user.key) {
|
||||
// API Key
|
||||
event.api_key = user.key;
|
||||
}
|
||||
else {
|
||||
event.username = user.username;
|
||||
}
|
||||
|
||||
// validate category
|
||||
self.storage.listFind( 'global/categories', { id: event.category }, function(err, item) {
|
||||
if (err || !item) {
|
||||
return self.doError('event', "Failed to create event: Category not found: " + event.category, callback);
|
||||
}
|
||||
|
||||
self.logDebug(6, "Creating new event: " + event.title, event);
|
||||
|
||||
self.storage.listUnshift( 'global/schedule', event, function(err) {
|
||||
if (err) {
|
||||
return self.doError('event', "Failed to create event: " + err, callback);
|
||||
}
|
||||
|
||||
self.logDebug(6, "Successfully created event: " + event.title, event);
|
||||
self.logTransaction('event_create', event.title, self.getClientInfo(args, { event: event }));
|
||||
self.logActivity('event_create', { event: event }, args);
|
||||
|
||||
callback({ code: 0, id: event.id });
|
||||
|
||||
// broadcast update to all websocket clients
|
||||
self.updateClientData( 'schedule' );
|
||||
|
||||
// create cursor for new event
|
||||
var now = Tools.normalizeTime( Tools.timeNow(), { sec: 0 } );
|
||||
self.state.cursors[ event.id ] = now;
|
||||
|
||||
// send new state data to all web clients
|
||||
self.authSocketEmit( 'update', { state: self.state } );
|
||||
|
||||
} ); // list insert
|
||||
} ); // find cat
|
||||
} ); // load session
|
||||
},
|
||||
|
||||
api_update_event: function(args, callback) {
|
||||
// update existing event
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
// validate optional event data parameters
|
||||
if (!this.requireValidEventData(params, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireValidUser(session, user, callback)) return;
|
||||
if (!self.requirePrivilege(user, "edit_events", callback)) return;
|
||||
if (params.abort_jobs && !self.requirePrivilege(user, "abort_events", callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
self.storage.listFind( 'global/schedule', { id: params.id }, function(err, event) {
|
||||
if (err || !event) {
|
||||
return self.doError('event', "Failed to locate event: " + params.id, callback);
|
||||
}
|
||||
|
||||
// validate category
|
||||
self.storage.listFind('global/categories', { id: params.category || event.category }, function (err, item) {
|
||||
if (err || !item) {
|
||||
return self.doError('event', "Failed to update event: Category not found: " + params.category, callback);
|
||||
}
|
||||
|
||||
if (!self.requireCategoryPrivilege(user, event.category, callback)) return;
|
||||
if (!self.requireGroupPrivilege(args, user, event.target, callback)) return;
|
||||
|
||||
params.modified = Tools.timeNow(true);
|
||||
|
||||
self.logDebug(6, "Updating event: " + event.title, params);
|
||||
|
||||
// pull cursor reset out of event object, for use later
|
||||
var new_cursor = 0;
|
||||
if (params.reset_cursor) {
|
||||
new_cursor = Tools.normalizeTime(params.reset_cursor - 60, { sec: 0 });
|
||||
delete params.reset_cursor;
|
||||
}
|
||||
|
||||
// pull abort flag out of event object, for use later
|
||||
var abort_jobs = 0;
|
||||
if (params.abort_jobs) {
|
||||
abort_jobs = params.abort_jobs;
|
||||
delete params.abort_jobs;
|
||||
}
|
||||
|
||||
self.storage.listFindUpdate( 'global/schedule', { id: params.id }, params, function(err) {
|
||||
if (err) {
|
||||
return self.doError('event', "Failed to update event: " + err, callback);
|
||||
}
|
||||
|
||||
// merge params into event, just so we have the full updated record
|
||||
for (var key in params) event[key] = params[key];
|
||||
|
||||
// optionally reset cursor
|
||||
if (new_cursor) {
|
||||
var dargs = Tools.getDateArgs( new_cursor );
|
||||
self.logDebug(6, "Resetting event cursor to: " + dargs.yyyy_mm_dd + ' ' + dargs.hh_mi_ss);
|
||||
self.state.cursors[ params.id ] = new_cursor;
|
||||
|
||||
// send new state data to all web clients
|
||||
self.authSocketEmit( 'update', { state: self.state } );
|
||||
}
|
||||
|
||||
self.logDebug(6, "Successfully updated event: " + event.id + " (" + event.title + ")");
|
||||
self.logTransaction('event_update', event.title, self.getClientInfo(args, { event: params }));
|
||||
self.logActivity('event_update', { event: params }, args);
|
||||
|
||||
// send response to web client
|
||||
callback({ code: 0 });
|
||||
|
||||
// broadcast update to all websocket clients
|
||||
self.updateClientData( 'schedule' );
|
||||
|
||||
// if event is disabled, abort all applicable jobs
|
||||
if (!event.enabled && abort_jobs) {
|
||||
var all_jobs = self.getAllActiveJobs(true);
|
||||
for (var key in all_jobs) {
|
||||
var job = all_jobs[key];
|
||||
if ((job.event == event.id) && !job.detached) {
|
||||
var msg = "Event '" + event.title + "' has been disabled.";
|
||||
self.logDebug(4, "Job " + job.id + " is being aborted: " + msg);
|
||||
self.abortJob({ id: job.id, reason: msg });
|
||||
} // matches event
|
||||
} // foreach job
|
||||
} // event disabled
|
||||
|
||||
// if this is a catch_up event and is being enabled, force scheduler to re-tick the minute
|
||||
var dargs = Tools.getDateArgs( new Date() );
|
||||
if (params.enabled && event.catch_up && !self.schedulerGraceTimer && !self.schedulerTicking && (dargs.sec != 59)) {
|
||||
self.schedulerMinuteTick( null, true );
|
||||
}
|
||||
|
||||
// check event queue
|
||||
if (self.eventQueue[event.id]) {
|
||||
if (event.queue) self.checkEventQueues( event.id );
|
||||
else self.deleteEventQueues( event.id );
|
||||
}
|
||||
} ); // update event
|
||||
} ); // find cat
|
||||
} ); // find event
|
||||
} ); // load session
|
||||
},
|
||||
|
||||
api_delete_event: function(args, callback) {
|
||||
// delete existing event
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireValidUser(session, user, callback)) return;
|
||||
if (!self.requirePrivilege(user, "delete_events", callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
self.storage.listFind( 'global/schedule', { id: params.id }, function(err, event) {
|
||||
if (err || !event) {
|
||||
return self.doError('event', "Failed to locate event: " + params.id, callback);
|
||||
}
|
||||
|
||||
if (!self.requireCategoryPrivilege(user, event.category, callback)) return;
|
||||
if (!self.requireGroupPrivilege(args, user, event.target, callback)) return;
|
||||
|
||||
// Do not allow deleting event if any active jobs
|
||||
var all_jobs = self.getAllActiveJobs(true);
|
||||
for (var key in all_jobs) {
|
||||
var job = all_jobs[key];
|
||||
if (job.event == params.id) {
|
||||
var err = "Still has running jobs";
|
||||
return self.doError('event', "Failed to delete event: " + err, callback);
|
||||
} // matches event
|
||||
} // foreach job
|
||||
|
||||
self.logDebug(6, "Deleting event: " + params.id);
|
||||
|
||||
self.storage.listFindDelete( 'global/schedule', { id: params.id }, function(err) {
|
||||
if (err) {
|
||||
return self.doError('event', "Failed to delete event: " + err, callback);
|
||||
}
|
||||
|
||||
self.logDebug(6, "Successfully deleted event: " + event.title, event);
|
||||
self.logTransaction('event_delete', event.title, self.getClientInfo(args, { event: event }));
|
||||
self.logActivity('event_delete', { event: event }, args);
|
||||
|
||||
callback({ code: 0 });
|
||||
|
||||
// broadcast update to all websocket clients
|
||||
self.updateClientData( 'schedule' );
|
||||
|
||||
// schedule event's activity log to be deleted at next maint run
|
||||
self.storage.expire( 'logs/events/' + event.id, Tools.timeNow(true) + 86400 );
|
||||
|
||||
// delete state data
|
||||
delete self.state.cursors[ event.id ];
|
||||
if (self.state.robins) delete self.state.robins[ event.id ];
|
||||
|
||||
// send new state data to all web clients
|
||||
self.authSocketEmit( 'update', { state: self.state } );
|
||||
|
||||
// check event queue
|
||||
if (self.eventQueue[event.id]) {
|
||||
self.deleteEventQueues( event.id );
|
||||
}
|
||||
|
||||
} ); // delete
|
||||
} ); // listFind
|
||||
} ); // load session
|
||||
},
|
||||
|
||||
api_run_event: function(args, callback) {
|
||||
// run event manually (via "Run" button in UI or by API Key)
|
||||
// can include any event overrides in params (such as 'now')
|
||||
var self = this;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
// default behavor: merge post params and query together
|
||||
// alt behavior (post_data): store post params into post_data
|
||||
var params = Tools.copyHash( args.query, true );
|
||||
if (args.query.post_data) params.post_data = args.params;
|
||||
else Tools.mergeHashInto( params, args.params );
|
||||
|
||||
var criteria = {};
|
||||
if (params.id) criteria.id = params.id;
|
||||
else if (params.title) criteria.title = params.title;
|
||||
else return this.doError('event', "Failed to locate event: No criteria specified", callback);
|
||||
|
||||
// validate optional event data parameters
|
||||
if (!this.requireValidEventData(params, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireValidUser(session, user, callback)) return;
|
||||
if (!self.requirePrivilege(user, "run_events", callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
self.storage.listFind( 'global/schedule', criteria, function(err, event) {
|
||||
if (err || !event) {
|
||||
return self.doError('event', "Failed to locate event: " + (params.id || params.title), callback);
|
||||
}
|
||||
|
||||
if (!self.requireCategoryPrivilege(user, event.category, callback)) return;
|
||||
if (!self.requireGroupPrivilege(args, user, event.target, callback)) return;
|
||||
|
||||
delete params.id;
|
||||
delete params.title;
|
||||
delete params.catch_up;
|
||||
delete params.category;
|
||||
delete params.multiplex;
|
||||
delete params.stagger;
|
||||
delete params.detached;
|
||||
delete params.queue;
|
||||
delete params.queue_max;
|
||||
delete params.max_children;
|
||||
delete params.session_id;
|
||||
|
||||
// allow for ¶ms/foo=bar and the like
|
||||
for (var key in params) {
|
||||
if (key.match(/^(\w+)\/(\w+)$/)) {
|
||||
var parent_key = RegExp.$1;
|
||||
var sub_key = RegExp.$2;
|
||||
if (!params[parent_key]) params[parent_key] = {};
|
||||
params[parent_key][sub_key] = params[key];
|
||||
delete params[key];
|
||||
}
|
||||
}
|
||||
|
||||
// allow sparsely populated event params in request
|
||||
if (params.params && event.params) {
|
||||
for (var key in event.params) {
|
||||
if (!(key in params.params)) params.params[key] = event.params[key];
|
||||
}
|
||||
}
|
||||
|
||||
var job = Tools.mergeHashes( Tools.copyHash(event, true), params );
|
||||
if (user.key) {
|
||||
// API Key
|
||||
job.source = "API Key ("+user.title+")";
|
||||
job.api_key = user.key;
|
||||
}
|
||||
else {
|
||||
job.source = "Manual ("+user.username+")";
|
||||
job.username = user.username;
|
||||
}
|
||||
|
||||
self.logDebug(6, "Running event manually: " + job.title, job);
|
||||
|
||||
self.launchOrQueueJob( job, function(err, jobs_launched) {
|
||||
if (err) {
|
||||
return self.doError('event', "Failed to launch event: " + err.message, callback);
|
||||
}
|
||||
|
||||
// multiple jobs may have been launched (multiplex)
|
||||
var ids = [];
|
||||
for (var idx = 0, len = jobs_launched.length; idx < len; idx++) {
|
||||
var job = jobs_launched[idx];
|
||||
var stub = { id: job.id, event: job.event };
|
||||
self.logTransaction('job_run', job.event_title, self.getClientInfo(args, stub));
|
||||
if (self.server.config.get('track_manual_jobs')) self.logActivity('job_run', stub, args);
|
||||
ids.push( job.id );
|
||||
}
|
||||
|
||||
var resp = { code: 0, ids: ids };
|
||||
if (!ids.length) resp.queue = self.eventQueue[event.id] || 1;
|
||||
callback(resp);
|
||||
} ); // launch job
|
||||
} ); // find event
|
||||
} ); // load session
|
||||
},
|
||||
|
||||
api_flush_event_queue: function(args, callback) {
|
||||
// flush event queue
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireValidUser(session, user, callback)) return;
|
||||
if (!self.requirePrivilege(user, "abort_events", callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
self.storage.listFind( 'global/schedule', { id: params.id }, function(err, event) {
|
||||
if (err || !event) {
|
||||
return self.doError('event', "Failed to locate event: " + params.id, callback);
|
||||
}
|
||||
|
||||
if (!self.requireCategoryPrivilege(user, event.category, callback)) return;
|
||||
if (!self.requireGroupPrivilege(args, user, event.target, callback)) return;
|
||||
|
||||
self.deleteEventQueues( params.id, function() {
|
||||
callback({ code: 0 });
|
||||
} );
|
||||
|
||||
} ); // listFind
|
||||
} ); // loadSession
|
||||
},
|
||||
|
||||
api_get_event_history: function(args, callback) {
|
||||
// get event history
|
||||
var self = this;
|
||||
var params = Tools.mergeHashes( args.params, args.query );
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireValidUser(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
self.storage.listFind( 'global/schedule', { id: params.id }, function(err, event) {
|
||||
if (err || !event) {
|
||||
return self.doError('event', "Failed to locate event: " + params.id, callback);
|
||||
}
|
||||
|
||||
if (!self.requireCategoryPrivilege(user, event.category, callback)) return;
|
||||
if (!self.requireGroupPrivilege(args, user, event.target, callback)) return;
|
||||
|
||||
self.storage.listGet( 'logs/events/' + params.id, parseInt(params.offset || 0), parseInt(params.limit || 100), function(err, items, list) {
|
||||
if (err) {
|
||||
// no rows found, not an error for this API
|
||||
return callback({ code: 0, rows: [], list: { length: 0 } });
|
||||
}
|
||||
|
||||
// success, return rows and list header
|
||||
callback({ code: 0, rows: items, list: list });
|
||||
|
||||
} ); // got data
|
||||
} ); // listFind
|
||||
} ); // load session
|
||||
},
|
||||
|
||||
api_get_history: function(args, callback) {
|
||||
// get list of completed jobs for ALL events (with pagination)
|
||||
var self = this;
|
||||
var params = Tools.mergeHashes( args.params, args.query );
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireValidUser(session, user, callback)) return;
|
||||
|
||||
// user may be limited to certain categories
|
||||
// but in order to keep pagination sane, just mask out IDs
|
||||
var privs = user.privileges;
|
||||
var cat_limited = (!privs.admin && privs.cat_limit);
|
||||
|
||||
self.storage.listGet( 'logs/completed', parseInt(params.offset || 0), parseInt(params.limit || 50), function(err, items, list) {
|
||||
if (err) {
|
||||
// no rows found, not an error for this API
|
||||
return callback({ code: 0, rows: [], list: { length: 0 } });
|
||||
}
|
||||
|
||||
// mask out IDs for cats that user shouldn't see
|
||||
if (cat_limited) items.forEach( function(item) {
|
||||
var priv_id = 'cat_' + item.category;
|
||||
if (!privs[priv_id]) item.id = '';
|
||||
} );
|
||||
|
||||
// success, return rows and list header
|
||||
callback({ code: 0, rows: items, list: list });
|
||||
} ); // got data
|
||||
} ); // loaded session
|
||||
}
|
||||
|
||||
} );
|
160
workspaces/base-workspace/Cronicle-0.8.61/lib/api/group.js
Executable file
|
@ -0,0 +1,160 @@
|
|||
// Cronicle API Layer - Server Groups
|
||||
// Copyright (c) 2015 Joseph Huckaby
|
||||
// Released under the MIT License
|
||||
|
||||
var fs = require('fs');
|
||||
var assert = require("assert");
|
||||
var async = require('async');
|
||||
|
||||
var Class = require("pixl-class");
|
||||
var Tools = require("pixl-tools");
|
||||
|
||||
module.exports = Class.create({
|
||||
|
||||
//
|
||||
// Server Groups:
|
||||
//
|
||||
|
||||
api_create_server_group: function(args, callback) {
|
||||
// add new server group
|
||||
var self = this;
|
||||
var group = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(group, {
|
||||
title: /\S/,
|
||||
regexp: /\S/
|
||||
}, callback)) return;
|
||||
|
||||
// make sure title doesn't contain HTML metacharacters
|
||||
if (group.title && group.title.match(/[<>]/)) {
|
||||
return this.doError('api', "Malformed title parameter: Cannot contain HTML metacharacters", callback);
|
||||
}
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireAdmin(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
if (group.id) group.id = group.id.toString().toLowerCase().replace(/\W+/g, '');
|
||||
if (!group.id) group.id = self.getUniqueID('g');
|
||||
|
||||
self.logDebug(6, "Creating new server group: " + group.title, group);
|
||||
|
||||
self.storage.listUnshift( 'global/server_groups', group, function(err) {
|
||||
if (err) {
|
||||
return self.doError('group', "Failed to create server group: " + err, callback);
|
||||
}
|
||||
|
||||
self.logDebug(6, "Successfully created server group: " + group.title, group);
|
||||
self.logTransaction('group_create', group.title, self.getClientInfo(args, { group: group }));
|
||||
self.logActivity('group_create', { group: group }, args);
|
||||
|
||||
callback({ code: 0, id: group.id });
|
||||
|
||||
// broadcast update to all websocket clients
|
||||
self.updateClientData( 'server_groups' );
|
||||
|
||||
// notify all slave servers about the change as well
|
||||
// this may have changed their master server eligibility
|
||||
self.slaveNotifyGroupChange();
|
||||
} ); // list insert
|
||||
} ); // load session
|
||||
},
|
||||
|
||||
api_update_server_group: function(args, callback) {
|
||||
// update existing server group
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
// make sure title doesn't contain HTML metacharacters
|
||||
if (params.title && params.title.match(/[<>]/)) {
|
||||
return this.doError('api', "Malformed title parameter: Cannot contain HTML metacharacters", callback);
|
||||
}
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireAdmin(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
self.logDebug(6, "Updating server group: " + params.id, params);
|
||||
|
||||
self.storage.listFindUpdate( 'global/server_groups', { id: params.id }, params, function(err, group) {
|
||||
if (err) {
|
||||
return self.doError('group', "Failed to update server group: " + err, callback);
|
||||
}
|
||||
|
||||
self.logDebug(6, "Successfully updated server group: " + group.title, group);
|
||||
self.logTransaction('group_update', group.title, self.getClientInfo(args, { group: group }));
|
||||
self.logActivity('group_update', { group: group }, args);
|
||||
|
||||
callback({ code: 0 });
|
||||
|
||||
// broadcast update to all websocket clients
|
||||
self.updateClientData( 'server_groups' );
|
||||
|
||||
// notify all slave servers about the change as well
|
||||
// this may have changed their master server eligibility
|
||||
self.slaveNotifyGroupChange();
|
||||
} );
|
||||
} );
|
||||
},
|
||||
|
||||
api_delete_server_group: function(args, callback) {
|
||||
// delete existing server group
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireAdmin(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
// Do not allow deleting group if any matching events in schedule
|
||||
self.storage.listFind( 'global/schedule', { target: params.id }, function(err, item) {
|
||||
if (item) {
|
||||
return self.doError('plugin', "Failed to delete server group: Still targeted by one or more events.", callback);
|
||||
}
|
||||
|
||||
self.logDebug(6, "Deleting server group: " + params.id, params);
|
||||
|
||||
self.storage.listFindDelete( 'global/server_groups', { id: params.id }, function(err, group) {
|
||||
if (err) {
|
||||
return self.doError('group', "Failed to delete server group: " + err, callback);
|
||||
}
|
||||
|
||||
self.logDebug(6, "Successfully deleted server group: " + group.title, group);
|
||||
self.logTransaction('group_delete', group.title, self.getClientInfo(args, { group: group }));
|
||||
self.logActivity('group_delete', { group: group }, args);
|
||||
|
||||
callback({ code: 0 });
|
||||
|
||||
// broadcast update to all websocket clients
|
||||
self.updateClientData( 'server_groups' );
|
||||
|
||||
// notify all slave servers about the change as well
|
||||
// this may have changed their master server eligibility
|
||||
self.slaveNotifyGroupChange();
|
||||
|
||||
} ); // listFindDelete (group)
|
||||
} ); // listFind (schedule)
|
||||
} ); // load session
|
||||
}
|
||||
|
||||
} );
|
558
workspaces/base-workspace/Cronicle-0.8.61/lib/api/job.js
Executable file
|
@ -0,0 +1,558 @@
|
|||
// Cronicle API Layer - Jobs
|
||||
// Copyright (c) 2015 Joseph Huckaby
|
||||
// Released under the MIT License
|
||||
|
||||
var fs = require('fs');
|
||||
var assert = require("assert");
|
||||
var async = require('async');
|
||||
|
||||
var Class = require("pixl-class");
|
||||
var Tools = require("pixl-tools");
|
||||
|
||||
module.exports = Class.create({
|
||||
|
||||
//
|
||||
// Jobs
|
||||
//
|
||||
|
||||
/*api_upload_job_log: function(args, callback) {
|
||||
// server-to-server log upload
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
var files = args.files;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
path: /^[\w\-\.\/]+$/,
|
||||
auth: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
if (params.auth != Tools.digestHex(params.path + this.server.config.get('secret_key'))) {
|
||||
return callback( "403 Forbidden", {}, "Authentication failure.\n" );
|
||||
}
|
||||
|
||||
if (!files.file1 || !files.file1.path) {
|
||||
return callback( "400 Bad Request", {}, "No upload data found.\n" );
|
||||
}
|
||||
|
||||
fs.readFile( files.file1.path, { encoding: null }, function(err, data) {
|
||||
if (err) {
|
||||
return callback( "400 Bad Request", {}, "Could not read upload data: " + err + "\n" );
|
||||
}
|
||||
|
||||
self.storage.put( params.path, data, function(err) {
|
||||
if (err) {
|
||||
self.logError('storage', "Failed to store job log: " + params.path + ": " + err);
|
||||
return;
|
||||
}
|
||||
|
||||
callback( "200 OK", {}, "Success.\n" );
|
||||
} ); // storage put
|
||||
} );
|
||||
},*/
|
||||
|
||||
api_get_job_log: function(args, callback) {
|
||||
// view job log (plain text or download)
|
||||
// client API, no auth
|
||||
var self = this;
|
||||
|
||||
if (!this.requireParams(args.query, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
var key = 'jobs/' + args.query.id + '/log.txt.gz';
|
||||
|
||||
self.storage.getStream( key, function(err, stream) {
|
||||
if (err) {
|
||||
return callback( "404 Not Found", {}, "(No log file found.)\n" );
|
||||
}
|
||||
|
||||
var headers = {
|
||||
'Content-Type': "text/plain; charset=utf-8",
|
||||
'Content-Encoding': "gzip"
|
||||
};
|
||||
|
||||
// optional download instead of view
|
||||
if (args.query.download) {
|
||||
headers['Content-disposition'] = "attachment; filename=Cronicle-Job-Log-" + args.query.id + '.txt';
|
||||
}
|
||||
|
||||
// pass stream to web server
|
||||
callback( "200 OK", headers, stream );
|
||||
} );
|
||||
},
|
||||
|
||||
api_get_live_job_log: function(args, callback) {
|
||||
// get live job job, as it is being written
|
||||
// client API, no auth
|
||||
var self = this;
|
||||
var query = args.query;
|
||||
|
||||
if (!this.requireParams(query, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
// see if log file exists on this server
|
||||
var log_file = this.server.config.get('log_dir') + '/jobs/' + query.id + '.log';
|
||||
|
||||
fs.stat( log_file, function(err, stats) {
|
||||
if (err) {
|
||||
return self.doError('job', "Failed to fetch job log: " + err, callback);
|
||||
}
|
||||
|
||||
var headers = { 'Content-Type': "text/plain" };
|
||||
|
||||
// optional download instead of view
|
||||
if (query.download) {
|
||||
headers['Content-disposition'] = "attachment; filename=Cronicle-Partial-Job-Log-" + query.id + '.txt';
|
||||
}
|
||||
|
||||
// get readable stream to file
|
||||
var stream = fs.createReadStream( log_file );
|
||||
|
||||
// stream to client as plain text
|
||||
callback( "200 OK", headers, stream );
|
||||
} );
|
||||
},
|
||||
|
||||
api_fetch_delete_job_log: function(args, callback) {
|
||||
// fetch and delete job log, part of finish process
|
||||
// server-to-server API, deletes log, requires secret key auth
|
||||
var self = this;
|
||||
var query = args.query;
|
||||
|
||||
if (!this.requireParams(query, {
|
||||
path: /^[\w\-\.\/]+\.log$/,
|
||||
auth: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
if (query.auth != Tools.digestHex(query.path + this.server.config.get('secret_key'))) {
|
||||
return callback( "403 Forbidden", {}, "Authentication failure.\n" );
|
||||
}
|
||||
|
||||
var log_file = query.path;
|
||||
|
||||
fs.stat( log_file, function(err, stats) {
|
||||
if (err) {
|
||||
return callback( "404 Not Found", {}, "Log file not found: "+log_file+".\n" );
|
||||
}
|
||||
|
||||
var headers = { 'Content-Type': "text/plain" };
|
||||
|
||||
// get readable stream to file
|
||||
var stream = fs.createReadStream( log_file );
|
||||
|
||||
// stream to client as plain text
|
||||
callback( "200 OK", headers, stream );
|
||||
|
||||
args.response.on('finish', function() {
|
||||
// only delete local log file once log is COMPLETELY sent
|
||||
self.logDebug(4, "Deleting log file: " + log_file);
|
||||
|
||||
fs.unlink( log_file, function(err) {
|
||||
// ignore error
|
||||
} );
|
||||
|
||||
} ); // response finish
|
||||
|
||||
} ); // fs.stat
|
||||
},
|
||||
|
||||
api_get_log_watch_auth: function(args, callback) {
|
||||
// generate auth token for watching live job log stream
|
||||
// (websocket to target server which may be a slave, hence might not have storage)
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireValidUser(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
var job = null;
|
||||
|
||||
// due to a race condition, the job may not be registered yet
|
||||
async.retry( { times: 20, interval: 250 },
|
||||
async.ensureAsync( function(callback) {
|
||||
job = self.findJob(params);
|
||||
return job ? callback() : callback("NOPE");
|
||||
} ),
|
||||
function(err) {
|
||||
if (err) return self.doError('job', "Failed to locate job for log watch auth: " + params.id, callback);
|
||||
if (!self.requireCategoryPrivilege(user, job.category, callback)) return;
|
||||
if (!self.requireGroupPrivilege(args, user, job.target, callback)) return;
|
||||
|
||||
// generate token
|
||||
var token = Tools.digestHex(params.id + self.server.config.get('secret_key'));
|
||||
|
||||
callback({ code: 0, token: token });
|
||||
}
|
||||
); // async.retry
|
||||
} );
|
||||
},
|
||||
|
||||
api_update_job: function(args, callback) {
|
||||
// update running job
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireValidUser(session, user, callback)) return;
|
||||
if (!self.requirePrivilege(user, "edit_events", callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
var job = self.findJob(params);
|
||||
if (!job) return self.doError('job', "Failed to locate job: " + params.id, callback);
|
||||
if (!self.requireCategoryPrivilege(user, job.category, callback)) return;
|
||||
if (!self.requireGroupPrivilege(args, user, job.target, callback)) return;
|
||||
|
||||
var result = self.updateJob(params);
|
||||
if (!result) return self.doError('job', "Failed to update job.", callback);
|
||||
|
||||
self.logTransaction('job_update', params.id, self.getClientInfo(args, params));
|
||||
|
||||
callback({ code: 0 });
|
||||
} );
|
||||
},
|
||||
|
||||
api_update_jobs: function(args, callback) {
|
||||
// update multiple running jobs, search based on criteria (plugin, category, event)
|
||||
// stash updates in 'updates' key
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireValidUser(session, user, callback)) return;
|
||||
if (!self.requirePrivilege(user, "edit_events", callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
var updates = params.updates;
|
||||
delete params.updates;
|
||||
|
||||
var all_jobs = self.getAllActiveJobs(true);
|
||||
var jobs_arr = [];
|
||||
for (var key in all_jobs) {
|
||||
jobs_arr.push( all_jobs[key] );
|
||||
}
|
||||
var jobs = Tools.findObjects( jobs_arr, params );
|
||||
var count = 0;
|
||||
|
||||
for (var idx = 0, len = jobs.length; idx < len; idx++) {
|
||||
var job = jobs[idx];
|
||||
if (!self.requireCategoryPrivilege(user, job.category, callback)) return;
|
||||
if (!self.requireGroupPrivilege(args, user, job.target, callback)) return;
|
||||
}
|
||||
|
||||
for (var idx = 0, len = jobs.length; idx < len; idx++) {
|
||||
var job = jobs[idx];
|
||||
var result = self.updateJob( Tools.mergeHashes( updates, { id: job.id } ) );
|
||||
if (result) {
|
||||
count++;
|
||||
self.logTransaction('job_update', job.id, self.getClientInfo(args, updates));
|
||||
}
|
||||
} // foreach job
|
||||
|
||||
callback({ code: 0, count: count });
|
||||
} );
|
||||
},
|
||||
|
||||
api_abort_job: function(args, callback) {
|
||||
// abort running job
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireValidUser(session, user, callback)) return;
|
||||
if (!self.requirePrivilege(user, "abort_events", callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
var job = self.findJob(params);
|
||||
if (!job) return self.doError('job', "Failed to locate job: " + params.id, callback);
|
||||
if (!self.requireCategoryPrivilege(user, job.category, callback)) return;
|
||||
if (!self.requireGroupPrivilege(args, user, job.target, callback)) return;
|
||||
|
||||
var reason = '';
|
||||
if (user.key) {
|
||||
// API Key
|
||||
reason = "Manually aborted by API Key: " + user.key + " (" + user.title + ")";
|
||||
}
|
||||
else {
|
||||
reason = "Manually aborted by user: " + user.username;
|
||||
}
|
||||
|
||||
var result = self.abortJob({
|
||||
id: params.id,
|
||||
reason: reason,
|
||||
no_rewind: 1 // don't rewind cursor for manually aborted jobs
|
||||
});
|
||||
if (!result) return self.doError('job', "Failed to abort job.", callback);
|
||||
|
||||
callback({ code: 0 });
|
||||
} );
|
||||
},
|
||||
|
||||
api_abort_jobs: function(args, callback) {
|
||||
// abort multiple running jobs, search based on criteria (plugin, category, event)
|
||||
// by default this WILL rewind catch_up events, unless 'no_rewind' is specified
|
||||
// this will NOT abort any detached jobs
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireValidUser(session, user, callback)) return;
|
||||
if (!self.requirePrivilege(user, "abort_events", callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
var reason = '';
|
||||
if (user.key) {
|
||||
// API Key
|
||||
reason = "Manually aborted by API Key: " + user.key + " (" + user.title + ")";
|
||||
}
|
||||
else {
|
||||
reason = "Manually aborted by user: " + user.username;
|
||||
}
|
||||
|
||||
var no_rewind = params.no_rewind || 0;
|
||||
delete params.no_rewind;
|
||||
|
||||
var all_jobs = self.getAllActiveJobs(true);
|
||||
var jobs_arr = [];
|
||||
for (var key in all_jobs) {
|
||||
jobs_arr.push( all_jobs[key] );
|
||||
}
|
||||
var jobs = Tools.findObjects( jobs_arr, params );
|
||||
var count = 0;
|
||||
|
||||
for (var idx = 0, len = jobs.length; idx < len; idx++) {
|
||||
var job = jobs[idx];
|
||||
if (!self.requireCategoryPrivilege(user, job.category, callback)) return;
|
||||
if (!self.requireGroupPrivilege(args, user, job.target, callback)) return;
|
||||
}
|
||||
|
||||
for (var idx = 0, len = jobs.length; idx < len; idx++) {
|
||||
var job = jobs[idx];
|
||||
if (!job.detached) {
|
||||
var result = self.abortJob({
|
||||
id: job.id,
|
||||
reason: reason,
|
||||
no_rewind: no_rewind
|
||||
});
|
||||
if (result) count++;
|
||||
}
|
||||
} // foreach job
|
||||
|
||||
callback({ code: 0, count: count });
|
||||
} );
|
||||
},
|
||||
|
||||
api_get_job_details: function(args, callback) {
|
||||
// get details for completed job
|
||||
// need_log: will fail unless job log is also in storage
|
||||
var self = this;
|
||||
var params = Tools.mergeHashes( args.params, args.query );
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireValidUser(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
// job log must be available for this to work
|
||||
self.storage.head( 'jobs/' + params.id + '/log.txt.gz', function(err, info) {
|
||||
if (err && params.need_log) {
|
||||
return self.doError('job', "Failed to fetch job details: " + err, callback);
|
||||
}
|
||||
|
||||
// now fetch job details
|
||||
self.storage.get( 'jobs/' + params.id, function(err, job) {
|
||||
if (err) {
|
||||
return self.doError('job', "Failed to fetch job details: " + err, callback);
|
||||
}
|
||||
|
||||
if (!self.requireCategoryPrivilege(user, job.category, callback)) return;
|
||||
if (!self.requireGroupPrivilege(args, user, job.target, callback)) return;
|
||||
|
||||
callback({ code: 0, job: job });
|
||||
} ); // job get
|
||||
} ); // log head
|
||||
} ); // session
|
||||
},
|
||||
|
||||
api_get_job_status: function(args, callback) {
|
||||
// get details for job in progress, or completed job
|
||||
// can be used for polling for completion, look for `complete` flag
|
||||
var self = this;
|
||||
var params = Tools.mergeHashes( args.params, args.query );
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireValidUser(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
// check live jobs first
|
||||
var all_jobs = self.getAllActiveJobs();
|
||||
var job = all_jobs[ params.id ];
|
||||
if (job) {
|
||||
if (!self.requireCategoryPrivilege(user, job.category, callback)) return;
|
||||
if (!self.requireGroupPrivilege(args, user, job.target, callback)) return;
|
||||
|
||||
return callback({
|
||||
code: 0,
|
||||
job: Tools.mergeHashes( job, {
|
||||
elapsed: Tools.timeNow() - job.time_start
|
||||
} )
|
||||
});
|
||||
} // found job
|
||||
|
||||
// TODO: Rare but possible race condition here...
|
||||
// Slave server may have removed job from activeJobs, and synced with master,
|
||||
// but before master created the job record
|
||||
|
||||
// no good? see if job completed...
|
||||
self.storage.get( 'jobs/' + params.id, function(err, job) {
|
||||
if (err) {
|
||||
return self.doError('job', "Failed to fetch job details: " + err, callback);
|
||||
}
|
||||
|
||||
if (!self.requireCategoryPrivilege(user, job.category, callback)) return;
|
||||
if (!self.requireGroupPrivilege(args, user, job.target, callback)) return;
|
||||
|
||||
callback({ code: 0, job: job });
|
||||
} ); // job get
|
||||
} ); // session
|
||||
},
|
||||
|
||||
api_delete_job: function(args, callback) {
|
||||
// delete all files for completed job
|
||||
var self = this;
|
||||
var params = Tools.mergeHashes( args.params, args.query );
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireAdmin(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
// fetch job details
|
||||
self.storage.get( 'jobs/' + params.id, function(err, job) {
|
||||
if (err) {
|
||||
return self.doError('job', "Failed to fetch job details: " + err, callback);
|
||||
}
|
||||
|
||||
var stub = {
|
||||
action: 'job_delete',
|
||||
id: job.id,
|
||||
event: job.event
|
||||
};
|
||||
|
||||
async.series(
|
||||
[
|
||||
function(callback) {
|
||||
// update event history
|
||||
// ignore error as this may fail for a variety of reasons
|
||||
self.storage.listFindReplace( 'logs/events/' + job.event, { id: job.id }, stub, function(err) { callback(); } );
|
||||
},
|
||||
function(callback) {
|
||||
// update global history
|
||||
// ignore error as this may fail for a variety of reasons
|
||||
self.storage.listFindReplace( 'logs/completed', { id: job.id }, stub, function(err) { callback(); } );
|
||||
},
|
||||
function(callback) {
|
||||
// delete job log
|
||||
// ignore error as this may fail for a variety of reasons
|
||||
self.storage.delete( 'jobs/' + job.id + '/log.txt.gz', function(err) { callback(); } );
|
||||
},
|
||||
function(callback) {
|
||||
// delete job details
|
||||
// this should never fail
|
||||
self.storage.delete( 'jobs/' + job.id, callback );
|
||||
}
|
||||
],
|
||||
function(err) {
|
||||
// check for error
|
||||
if (err) {
|
||||
return self.doError('job', "Failed to delete job: " + err, callback);
|
||||
}
|
||||
|
||||
// add note to admin log
|
||||
self.logActivity('job_delete', stub, args);
|
||||
|
||||
// log transaction
|
||||
self.logTransaction('job_delete', job.id, self.getClientInfo(args));
|
||||
|
||||
// and we're done
|
||||
callback({ code: 0 });
|
||||
}
|
||||
); // async.series
|
||||
} ); // job get
|
||||
} ); // session
|
||||
},
|
||||
|
||||
api_get_active_jobs: function(args, callback) {
|
||||
// get all active jobs in progress
|
||||
var self = this;
|
||||
var params = Tools.mergeHashes( args.params, args.query );
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireValidUser(session, user, callback)) return;
|
||||
|
||||
return callback({
|
||||
code: 0,
|
||||
jobs: self.getAllActiveJobs()
|
||||
});
|
||||
} ); // session
|
||||
}
|
||||
|
||||
} );
|
235
workspaces/base-workspace/Cronicle-0.8.61/lib/api/plugin.js
Executable file
|
@ -0,0 +1,235 @@
|
|||
// Cronicle API Layer - Plugins
|
||||
// Copyright (c) 2015 Joseph Huckaby
|
||||
// Released under the MIT License
|
||||
|
||||
var fs = require('fs');
|
||||
var sqparse = require('shell-quote').parse;
|
||||
|
||||
var Class = require("pixl-class");
|
||||
var Tools = require("pixl-tools");
|
||||
|
||||
module.exports = Class.create({
|
||||
|
||||
//
|
||||
// Plugins:
|
||||
//
|
||||
|
||||
api_get_plugins: function(args, callback) {
|
||||
// get list of plugins (with pagination)
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireValidUser(session, user, callback)) return;
|
||||
|
||||
self.storage.listGet( 'global/plugins', parseInt(params.offset || 0), parseInt(params.limit || 0), function(err, items, list) {
|
||||
if (err) {
|
||||
// no plugins found, not an error for this API
|
||||
return callback({ code: 0, rows: [], list: { length: 0 } });
|
||||
}
|
||||
|
||||
// success, return plugins and list header
|
||||
callback({ code: 0, rows: items, list: list });
|
||||
} ); // got plugin list
|
||||
} ); // loaded session
|
||||
},
|
||||
|
||||
api_create_plugin: function(args, callback) {
|
||||
// add new plugin
|
||||
var self = this;
|
||||
var plugin = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(plugin, {
|
||||
title: /\S/,
|
||||
command: /\S/
|
||||
}, callback)) return;
|
||||
|
||||
// make sure title doesn't contain HTML metacharacters
|
||||
if (plugin.title && plugin.title.match(/[<>]/)) {
|
||||
return this.doError('api', "Malformed title parameter: Cannot contain HTML metacharacters", callback);
|
||||
}
|
||||
|
||||
if (!this.requireValidPluginCommand(plugin.command, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireAdmin(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
if (plugin.id) plugin.id = plugin.id.toString().toLowerCase().replace(/\W+/g, '');
|
||||
if (!plugin.id) plugin.id = self.getUniqueID('p');
|
||||
|
||||
plugin.params = plugin.params || [];
|
||||
plugin.username = user.username;
|
||||
plugin.created = plugin.modified = Tools.timeNow(true);
|
||||
|
||||
self.logDebug(6, "Creating new plugin: " + plugin.title, plugin);
|
||||
|
||||
self.storage.listUnshift( 'global/plugins', plugin, function(err) {
|
||||
if (err) {
|
||||
return self.doError('plugin', "Failed to create plugin: " + err, callback);
|
||||
}
|
||||
|
||||
self.logDebug(6, "Successfully created plugin: " + plugin.title, plugin);
|
||||
self.logTransaction('plugin_create', plugin.title, self.getClientInfo(args, { plugin: plugin }));
|
||||
self.logActivity('plugin_create', { plugin: plugin }, args);
|
||||
|
||||
callback({ code: 0, id: plugin.id });
|
||||
|
||||
// broadcast update to all websocket clients
|
||||
self.updateClientData( 'plugins' );
|
||||
} ); // list insert
|
||||
} ); // load session
|
||||
},
|
||||
|
||||
api_update_plugin: function(args, callback) {
|
||||
// update existing plugin
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
// make sure title doesn't contain HTML metacharacters
|
||||
if (params.title && params.title.match(/[<>]/)) {
|
||||
return this.doError('api', "Malformed title parameter: Cannot contain HTML metacharacters", callback);
|
||||
}
|
||||
|
||||
if (params.command) {
|
||||
if (!this.requireValidPluginCommand(params.command, callback)) return;
|
||||
}
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireAdmin(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
self.storage.listFind( 'global/plugins', { id: params.id }, function(err, plugin) {
|
||||
if (err || !plugin) {
|
||||
return self.doError('event', "Failed to locate plugin: " + params.id, callback);
|
||||
}
|
||||
|
||||
params.modified = Tools.timeNow(true);
|
||||
|
||||
self.logDebug(6, "Updating plugin: " + plugin.title, params);
|
||||
|
||||
// pull abort flag out of event object, for use later
|
||||
var abort_jobs = 0;
|
||||
if (params.abort_jobs) {
|
||||
abort_jobs = params.abort_jobs;
|
||||
delete params.abort_jobs;
|
||||
}
|
||||
|
||||
self.storage.listFindUpdate( 'global/plugins', { id: params.id }, params, function(err) {
|
||||
if (err) {
|
||||
return self.doError('plugin', "Failed to update plugin: " + err, callback);
|
||||
}
|
||||
|
||||
// merge params into plugin, just so we have the full updated record
|
||||
for (var key in params) plugin[key] = params[key];
|
||||
|
||||
self.logDebug(6, "Successfully updated plugin: " + plugin.title, params);
|
||||
self.logTransaction('plugin_update', plugin.title, self.getClientInfo(args, { plugin: params }));
|
||||
self.logActivity('plugin_update', { plugin: params }, args);
|
||||
|
||||
callback({ code: 0 });
|
||||
|
||||
// broadcast update to all websocket clients
|
||||
self.updateClientData( 'plugins' );
|
||||
|
||||
// if plugin is disabled, abort all applicable jobs
|
||||
if (!plugin.enabled && abort_jobs) {
|
||||
var all_jobs = self.getAllActiveJobs(true);
|
||||
for (var key in all_jobs) {
|
||||
var job = all_jobs[key];
|
||||
if ((job.plugin == plugin.id) && !job.detached) {
|
||||
var msg = "Plugin '" + plugin.title + "' has been disabled.";
|
||||
self.logDebug(4, "Job " + job.id + " is being aborted: " + msg);
|
||||
self.abortJob({ id: job.id, reason: msg });
|
||||
} // matches plugin
|
||||
} // foreach job
|
||||
} // plugin disabled
|
||||
|
||||
// if plugin is being enabled, force scheduler to re-tick the minute
|
||||
var dargs = Tools.getDateArgs( new Date() );
|
||||
if (params.enabled && !self.schedulerGraceTimer && !self.schedulerTicking && (dargs.sec != 59)) {
|
||||
self.schedulerMinuteTick( null, true );
|
||||
}
|
||||
|
||||
} ); // update plugin
|
||||
} ); // find plugin
|
||||
} ); // load session
|
||||
},
|
||||
|
||||
api_delete_plugin: function(args, callback) {
|
||||
// delete existing plugin
|
||||
var self = this;
|
||||
var params = args.params;
|
||||
if (!this.requireMaster(args, callback)) return;
|
||||
|
||||
if (!this.requireParams(params, {
|
||||
id: /^\w+$/
|
||||
}, callback)) return;
|
||||
|
||||
this.loadSession(args, function(err, session, user) {
|
||||
if (err) return self.doError('session', err.message, callback);
|
||||
if (!self.requireAdmin(session, user, callback)) return;
|
||||
|
||||
args.user = user;
|
||||
args.session = session;
|
||||
|
||||
// Do not allow deleting plugin if any matching events in schedule
|
||||
self.storage.listFind( 'global/schedule', { plugin: params.id }, function(err, item) {
|
||||
if (item) {
|
||||
return self.doError('plugin', "Failed to delete plugin: Still assigned to one or more events.", callback);
|
||||
}
|
||||
|
||||
self.logDebug(6, "Deleting plugin: " + params.id);
|
||||
|
||||
// Okay to delete
|
||||
self.storage.listFindDelete( 'global/plugins', { id: params.id }, function(err, plugin) {
|
||||
if (err) {
|
||||
return self.doError('plugin', "Failed to delete plugin: " + err, callback);
|
||||
}
|
||||
|
||||
self.logDebug(6, "Successfully deleted plugin: " + plugin.title, plugin);
|
||||
self.logTransaction('plugin_delete', plugin.title, self.getClientInfo(args, { plugin: plugin }));
|
||||
self.logActivity('plugin_delete', { plugin: plugin }, args);
|
||||
|
||||
callback({ code: 0 });
|
||||
|
||||
// broadcast update to all websocket clients
|
||||
self.updateClientData( 'plugins' );
|
||||
|
||||
} ); // listFindDelete
|
||||
} ); // listFind
|
||||
} ); // load session
|
||||
},
|
||||
|
||||
requireValidPluginCommand: function(command, callback) {
|
||||
// make sure plugin command is valid
|
||||
if (command.match(/\s+(.+)$/)) {
|
||||
var cargs_raw = RegExp.$1;
|
||||
var cargs = sqparse( cargs_raw, {} );
|
||||
|
||||
for (var idx = 0, len = cargs.length; idx < len; idx++) {
|
||||
var carg = cargs[idx];
|
||||
if (typeof(carg) == 'object') {
|
||||
return this.doError('plugin', "Plugin executable cannot contain any shell redirects or pipes.", callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} );
|
553
workspaces/base-workspace/Cronicle-0.8.61/lib/comm.js
Executable file
|
@ -0,0 +1,553 @@
|
|||
// Cronicle Server Communication Layer
|
||||
// Copyright (c) 2015 Joseph Huckaby
|
||||
// Released under the MIT License
|
||||
|
||||
var cp = require('child_process');
|
||||
var dns = require("dns");
|
||||
var SocketIO = require('socket.io');
|
||||
var SocketIOClient = require('socket.io-client');
|
||||
|
||||
var Class = require("pixl-class");
|
||||
var Tools = require("pixl-tools");
|
||||
|
||||
module.exports = Class.create({
|
||||
|
||||
slaves: null,
|
||||
sockets: null,
|
||||
|
||||
setupCluster: function() {
|
||||
// establish communication channel with all slaves
|
||||
var self = this;
|
||||
|
||||
// slaves are servers the master can send jobs to
|
||||
this.slaves = {};
|
||||
|
||||
// we're a slave too (but no socket needed)
|
||||
this.slaves[ this.server.hostname ] = {
|
||||
master: 1,
|
||||
hostname: this.server.hostname
|
||||
};
|
||||
|
||||
// add any registered slaves
|
||||
this.storage.listGet( 'global/servers', 0, 0, function(err, servers) {
|
||||
if (err) servers = [];
|
||||
for (var idx = 0, len = servers.length; idx < len; idx++) {
|
||||
var server = servers[idx];
|
||||
self.addServer( server );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
addServer: function(server, args) {
|
||||
// add new server to cluster
|
||||
var self = this;
|
||||
if (this.slaves[ server.hostname ]) return;
|
||||
|
||||
this.logDebug(5, "Adding slave to cluster: " + server.hostname + " (" + (server.ip || 'n/a') + ")");
|
||||
|
||||
var slave = {
|
||||
hostname: server.hostname,
|
||||
ip: server.ip || ''
|
||||
};
|
||||
|
||||
// connect via socket.io
|
||||
this.connectToSlave(slave);
|
||||
|
||||
// add slave to cluster
|
||||
this.slaves[ slave.hostname ] = slave;
|
||||
|
||||
// notify clients of the server change
|
||||
this.authSocketEmit( 'update', { servers: this.getAllServers() } );
|
||||
|
||||
// log activity for new server
|
||||
this.logActivity( 'server_add', { hostname: slave.hostname, ip: slave.ip || '' }, args );
|
||||
},
|
||||
|
||||
connectToSlave: function(slave) {
|
||||
// establish communication with slave via socket.io
|
||||
var self = this;
|
||||
var port = this.web.config.get('http_port');
|
||||
|
||||
var url = '';
|
||||
if (this.server.config.get('server_comm_use_hostnames')) {
|
||||
url = 'http://' + slave.hostname + ':' + port;
|
||||
}
|
||||
else {
|
||||
url = 'http://' + (slave.ip || slave.hostname) + ':' + port;
|
||||
}
|
||||
|
||||
this.logDebug(8, "Connecting to slave via socket.io: " + url);
|
||||
|
||||
var socket = new SocketIOClient( url, {
|
||||
multiplex: false,
|
||||
forceNew: true,
|
||||
reconnection: false,
|
||||
// reconnectionDelay: 1000,
|
||||
// reconnectionDelayMax: 1000,
|
||||
// reconnectionDelayMax: this.server.config.get('master_ping_freq') * 1000,
|
||||
// reconnectionAttempts: Infinity,
|
||||
// randomizationFactor: 0,
|
||||
timeout: 5000
|
||||
} );
|
||||
|
||||
socket.on('connect', function() {
|
||||
self.logDebug(6, "Successfully connected to slave: " + slave.hostname);
|
||||
|
||||
var now = Tools.timeNow(true);
|
||||
var token = Tools.digestHex( self.server.hostname + now + self.server.config.get('secret_key') );
|
||||
|
||||
// authenticate server-to-server with time-based token
|
||||
socket.emit( 'authenticate', {
|
||||
token: token,
|
||||
now: now,
|
||||
master_hostname: self.server.hostname
|
||||
} );
|
||||
|
||||
// remove disabled flag, in case this is a reconnect
|
||||
if (slave.disabled) {
|
||||
delete slave.disabled;
|
||||
self.logDebug(5, "Marking slave as enabled: " + slave.hostname);
|
||||
|
||||
// log activity for this
|
||||
self.logActivity( 'server_enable', { hostname: slave.hostname, ip: slave.ip || '' } );
|
||||
|
||||
// notify clients of the server change
|
||||
self.authSocketEmit( 'update', { servers: self.getAllServers() } );
|
||||
} // disabled
|
||||
|
||||
// reset reconnect delay
|
||||
delete slave.socketReconnectDelay;
|
||||
} );
|
||||
|
||||
/*socket.on('reconnectingDISABLED', function(err) {
|
||||
self.logDebug(6, "Reconnecting to slave: " + slave.hostname);
|
||||
|
||||
// mark slave as disabled to avoid sending it new jobs
|
||||
if (!slave.disabled) {
|
||||
slave.disabled = true;
|
||||
self.logDebug(5, "Marking slave as disabled: " + slave.hostname);
|
||||
|
||||
// notify clients of the server change
|
||||
self.authSocketEmit( 'update', { servers: self.getAllServers() } );
|
||||
|
||||
// if slave had active jobs, move them to limbo
|
||||
if (slave.active_jobs) {
|
||||
for (var id in slave.active_jobs) {
|
||||
self.logDebug(5, "Moving job to limbo: " + id);
|
||||
self.deadJobs[id] = slave.active_jobs[id];
|
||||
self.deadJobs[id].time_dead = Tools.timeNow(true);
|
||||
}
|
||||
delete slave.active_jobs;
|
||||
}
|
||||
} // not disabled yet
|
||||
} );*/
|
||||
|
||||
socket.on('disconnect', function() {
|
||||
if (!socket._pixl_disconnected) {
|
||||
self.logError('server', "Slave disconnected unexpectedly: " + slave.hostname);
|
||||
self.reconnectToSlave(slave);
|
||||
}
|
||||
else {
|
||||
self.logDebug(5, "Slave disconnected: " + slave.hostname, socket.id);
|
||||
}
|
||||
} );
|
||||
socket.on('error', function(err) {
|
||||
self.logError('server', "Slave socket error: " + slave.hostname + ": " + err);
|
||||
} );
|
||||
socket.on('connect_error', function(err) {
|
||||
self.logError('server', "Slave connection failed: " + slave.hostname + ": " + err);
|
||||
if (!socket._pixl_disconnected) self.reconnectToSlave(slave);
|
||||
} );
|
||||
socket.on('connect_timeout', function() {
|
||||
self.logError('server', "Slave connection timeout: " + slave.hostname);
|
||||
} );
|
||||
/*socket.on('reconnect_error', function(err) {
|
||||
self.logError('server', "Slave reconnection failed: " + slave.hostname + ": " + err);
|
||||
} );
|
||||
socket.on('reconnect_failed', function() {
|
||||
self.logError('server', "Slave retries exhausted: " + slave.hostname);
|
||||
} );*/
|
||||
|
||||
// Custom commands:
|
||||
|
||||
socket.on('status', function(status) {
|
||||
self.logDebug(10, "Got status from slave: " + slave.hostname, status);
|
||||
Tools.mergeHashInto( slave, status );
|
||||
self.checkServerClock(slave);
|
||||
self.checkServerJobs(slave);
|
||||
|
||||
// sanity check (should never happen)
|
||||
if (slave.master) self.masterConflict(slave);
|
||||
} );
|
||||
|
||||
socket.on('finish_job', function(job) {
|
||||
self.finishJob( job );
|
||||
} );
|
||||
|
||||
socket.on('fetch_job_log', function(job) {
|
||||
self.fetchStoreJobLog( job );
|
||||
} );
|
||||
|
||||
socket.on('auth_failure', function(data) {
|
||||
var err_msg = "Authentication failure, cannot add slave: " + slave.hostname + " ("+data.description+")";
|
||||
self.logError('server', err_msg);
|
||||
self.logActivity('error', { description: err_msg } );
|
||||
self.removeServer( slave );
|
||||
} );
|
||||
|
||||
slave.socket = socket;
|
||||
},
|
||||
|
||||
reconnectToSlave: function(slave) {
|
||||
// reconnect to slave after socket error
|
||||
var self = this;
|
||||
|
||||
// mark slave as disabled to avoid sending it new jobs
|
||||
if (!slave.disabled) {
|
||||
slave.disabled = true;
|
||||
self.logDebug(5, "Marking slave as disabled: " + slave.hostname);
|
||||
|
||||
// log activity for this
|
||||
self.logActivity( 'server_disable', { hostname: slave.hostname, ip: slave.ip || '' } );
|
||||
|
||||
// notify clients of the server change
|
||||
self.authSocketEmit( 'update', { servers: self.getAllServers() } );
|
||||
|
||||
// if slave had active jobs, move them to limbo
|
||||
if (slave.active_jobs) {
|
||||
for (var id in slave.active_jobs) {
|
||||
self.logDebug(5, "Moving job to limbo: " + id);
|
||||
self.deadJobs[id] = slave.active_jobs[id];
|
||||
self.deadJobs[id].time_dead = Tools.timeNow(true);
|
||||
}
|
||||
delete slave.active_jobs;
|
||||
}
|
||||
} // not disabled yet
|
||||
|
||||
// slowly back off retries to N sec to avoid spamming the logs too much
|
||||
if (!slave.socketReconnectDelay) slave.socketReconnectDelay = 0;
|
||||
if (slave.socketReconnectDelay < this.server.config.get('master_ping_freq')) slave.socketReconnectDelay++;
|
||||
|
||||
slave.socketReconnectTimer = setTimeout( function() {
|
||||
delete slave.socketReconnectTimer;
|
||||
if (!self.server.shut) {
|
||||
self.logDebug(6, "Reconnecting to slave: " + slave.hostname);
|
||||
self.connectToSlave(slave);
|
||||
}
|
||||
}, slave.socketReconnectDelay * 1000 );
|
||||
},
|
||||
|
||||
checkServerClock: function(slave) {
|
||||
// make sure slave clock is close to ours
|
||||
if (!slave.clock_drift) slave.clock_drift = 0;
|
||||
var now = Tools.timeNow();
|
||||
var drift = Math.abs( now - slave.epoch );
|
||||
|
||||
if ((drift >= 10) && (slave.clock_drift < 10)) {
|
||||
var err_msg = "Server clock is " + Tools.shortFloat(drift) + " seconds out of sync: " + slave.hostname;
|
||||
this.logError('server', err_msg);
|
||||
this.logActivity('error', { description: err_msg } );
|
||||
}
|
||||
|
||||
slave.clock_drift = drift;
|
||||
},
|
||||
|
||||
checkServerJobs: function(slave) {
|
||||
// remove any slave jobs from limbo, if applicable
|
||||
if (slave.active_jobs) {
|
||||
for (var id in slave.active_jobs) {
|
||||
if (this.deadJobs[id]) {
|
||||
this.logDebug(5, "Taking job out of limbo: " + id);
|
||||
delete this.deadJobs[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
removeServer: function(server, args) {
|
||||
// remove server from cluster
|
||||
var slave = this.slaves[ server.hostname ];
|
||||
if (!slave) return;
|
||||
|
||||
this.logDebug(5, "Removing slave from cluster: " + slave.hostname + " (" + (slave.ip || 'n/a') + ")");
|
||||
|
||||
// Deal with active jobs that were on the lost server
|
||||
// Stick them in limbo with a short timeout
|
||||
if (slave.active_jobs) {
|
||||
for (var id in slave.active_jobs) {
|
||||
this.logDebug(5, "Moving job to limbo: " + id);
|
||||
this.deadJobs[id] = slave.active_jobs[id];
|
||||
this.deadJobs[id].time_dead = Tools.timeNow(true);
|
||||
}
|
||||
delete slave.active_jobs;
|
||||
}
|
||||
|
||||
if (slave.socket) {
|
||||
slave.socket._pixl_disconnected = true;
|
||||
slave.socket.off('disconnect');
|
||||
slave.socket.disconnect();
|
||||
delete slave.socket;
|
||||
}
|
||||
if (slave.socketReconnectTimer) {
|
||||
clearTimeout( slave.socketReconnectTimer );
|
||||
delete slave.socketReconnectTimer;
|
||||
}
|
||||
|
||||
delete this.slaves[ slave.hostname ];
|
||||
|
||||
// notify clients of the server change
|
||||
this.authSocketEmit( 'update', { servers: this.getAllServers() } );
|
||||
|
||||
// log activity for lost server
|
||||
this.logActivity( 'server_remove', { hostname: slave.hostname }, args );
|
||||
},
|
||||
|
||||
startSocketListener: function() {
|
||||
// start listening for websocket connections
|
||||
this.numSocketClients = 0;
|
||||
this.sockets = {};
|
||||
this.io = SocketIO();
|
||||
|
||||
this.io.attach( this.web.http );
|
||||
if (this.web.https) this.io.attach( this.web.https );
|
||||
|
||||
this.io.on('connection', this.handleNewSocket.bind(this) );
|
||||
},
|
||||
|
||||
handleNewSocket: function(socket) {
|
||||
// handle new socket connection from socket.io
|
||||
// this could be from a web browser, or a server-to-server conn
|
||||
var self = this;
|
||||
var ip = socket.request.connection.remoteAddress || socket.client.conn.remoteAddress || 'Unknown';
|
||||
|
||||
socket._pixl_auth = false;
|
||||
|
||||
this.numSocketClients++;
|
||||
this.sockets[ socket.id ] = socket;
|
||||
this.logDebug(5, "New socket.io client connected: " + socket.id + " (IP: " + ip + ")");
|
||||
|
||||
socket.on('authenticate', function(params) {
|
||||
// client is trying to authenticate
|
||||
if (params.master_hostname && params.now && params.token) {
|
||||
// master-to-slave connection (we are the slave)
|
||||
var correct_token = Tools.digestHex( params.master_hostname + params.now + self.server.config.get('secret_key') );
|
||||
if (params.token != correct_token) {
|
||||
socket.emit( 'auth_failure', { description: "Secret Keys do not match." } );
|
||||
return;
|
||||
}
|
||||
/*if (Math.abs(Tools.timeNow() - params.now) > 60) {
|
||||
socket.emit( 'auth_failure', { description: "Server clocks are too far out of sync." } );
|
||||
return;
|
||||
}*/
|
||||
|
||||
self.logDebug(4, "Socket client " + socket.id + " has authenticated via secret key (IP: "+ip+")");
|
||||
socket._pixl_auth = true;
|
||||
socket._pixl_master = true;
|
||||
|
||||
// force multi-server init (quick startup: to skip waiting for the tock)
|
||||
self.logDebug(3, "Master server is: " + params.master_hostname);
|
||||
|
||||
// set some flags
|
||||
self.multi.cluster = true;
|
||||
self.multi.masterHostname = params.master_hostname;
|
||||
self.multi.masterIP = ip;
|
||||
self.multi.master = false;
|
||||
self.multi.lastPingReceived = Tools.timeNow(true);
|
||||
|
||||
if (!self.multi.slave) self.goSlave();
|
||||
|
||||
// need to recheck this
|
||||
self.checkMasterEligibility();
|
||||
} // secret_key
|
||||
else {
|
||||
// web client to server connection
|
||||
self.storage.get( 'sessions/' + params.token, function(err, data) {
|
||||
if (err) {
|
||||
self.logError('socket', "Socket client " + socket.id + " failed to authenticate (IP: "+ip+")");
|
||||
socket.emit( 'auth_failure', { description: "Session not found." } );
|
||||
}
|
||||
else {
|
||||
self.logDebug(4, "Socket client " + socket.id + " has authenticated via user session (IP: "+ip+")");
|
||||
socket._pixl_auth = true;
|
||||
}
|
||||
} );
|
||||
}
|
||||
} );
|
||||
|
||||
socket.on('launch_job', function(job) {
|
||||
// launch job (server-to-server comm)
|
||||
if (socket._pixl_auth) self.launchLocalJob( job );
|
||||
} );
|
||||
|
||||
socket.on('abort_job', function(stub) {
|
||||
// abort job (server-to-server comm)
|
||||
if (socket._pixl_auth) self.abortLocalJob( stub );
|
||||
} );
|
||||
|
||||
socket.on('update_job', function(stub) {
|
||||
// update job (server-to-server comm)
|
||||
if (socket._pixl_auth) self.updateLocalJob( stub );
|
||||
} );
|
||||
|
||||
socket.on('restart_server', function(args) {
|
||||
// restart server (server-to-server comm)
|
||||
if (socket._pixl_auth) self.restartLocalServer(args);
|
||||
} );
|
||||
|
||||
socket.on('shutdown_server', function(args) {
|
||||
// shut down server (server-to-server comm)
|
||||
if (socket._pixl_auth) self.shutdownLocalServer(args);
|
||||
} );
|
||||
|
||||
socket.on('watch_job_log', function(args) {
|
||||
// tail -f job log
|
||||
self.watchJobLog(args, socket);
|
||||
} );
|
||||
|
||||
socket.on('groups_changed', function(args) {
|
||||
// recheck master server eligibility
|
||||
self.logDebug(4, "Server groups have changed, rechecking master eligibility");
|
||||
self.checkMasterEligibility();
|
||||
} );
|
||||
|
||||
socket.on('logout', function(args) {
|
||||
// user wants out? okay then
|
||||
socket._pixl_auth = false;
|
||||
socket._pixl_master = false;
|
||||
} );
|
||||
|
||||
socket.on('master_ping', function(args) {
|
||||
// master has given dobby a ping!
|
||||
self.logDebug(10, "Received ping from master server");
|
||||
self.multi.lastPingReceived = Tools.timeNow(true);
|
||||
} );
|
||||
|
||||
socket.on('error', function(err) {
|
||||
self.logError('socket', "Client socket error: " + socket.id + ": " + err);
|
||||
} );
|
||||
|
||||
socket.on('disconnect', function() {
|
||||
// client disconnected
|
||||
socket._pixl_disconnected = true;
|
||||
self.numSocketClients--;
|
||||
delete self.sockets[ socket.id ];
|
||||
self.logDebug(5, "Socket.io client disconnected: " + socket.id + " (IP: " + ip + ")");
|
||||
} );
|
||||
},
|
||||
|
||||
sendMasterPings: function() {
|
||||
// send a ping to all slaves
|
||||
this.slaveBroadcastAll('master_ping');
|
||||
},
|
||||
|
||||
slaveNotifyGroupChange: function() {
|
||||
// notify all slaves that server groups have changed
|
||||
this.slaveBroadcastAll('groups_changed');
|
||||
},
|
||||
|
||||
slaveBroadcastAll: function(key, data) {
|
||||
// broadcast message to all slaves
|
||||
if (!this.multi.master) return;
|
||||
|
||||
for (var hostname in this.slaves) {
|
||||
var slave = this.slaves[hostname];
|
||||
if (slave.socket) {
|
||||
slave.socket.emit(key, data || {});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getAllServers: function() {
|
||||
// get combo hash of all UDP-managed servers, and any manually added slaves
|
||||
if (!this.multi.master) return null;
|
||||
var servers = {};
|
||||
var now = Tools.timeNow(true);
|
||||
|
||||
// add us first (the master)
|
||||
servers[ this.server.hostname ] = {
|
||||
hostname: this.server.hostname,
|
||||
ip: this.server.ip,
|
||||
master: 1,
|
||||
uptime: now - (this.server.started || now),
|
||||
data: this.multi.data || {},
|
||||
disabled: 0
|
||||
};
|
||||
|
||||
// then add all slaves
|
||||
for (var hostname in this.slaves) {
|
||||
var slave = this.slaves[hostname];
|
||||
if (!servers[hostname]) {
|
||||
servers[hostname] = {
|
||||
hostname: hostname,
|
||||
ip: slave.ip || '',
|
||||
master: 0,
|
||||
uptime: slave.uptime || 0,
|
||||
data: slave.data || {},
|
||||
disabled: slave.disabled || 0
|
||||
};
|
||||
} // unique hostname
|
||||
} // foreach slave
|
||||
|
||||
return servers;
|
||||
},
|
||||
|
||||
shutdownLocalServer: function(args) {
|
||||
// shut down local server
|
||||
if (this.server.debug) {
|
||||
this.logDebug(5, "Skipping shutdown command, as we're in debug mode.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.logDebug(1, "Shutting down server: " + (args.reason || 'Unknown reason'));
|
||||
|
||||
// issue shutdown command
|
||||
this.server.shutdown();
|
||||
},
|
||||
|
||||
restartLocalServer: function(args) {
|
||||
// restart server, but only if in daemon mode
|
||||
if (this.server.debug) {
|
||||
this.logDebug(5, "Skipping restart command, as we're in debug mode.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.logDebug(1, "Restarting server: " + (args.reason || 'Unknown reason'));
|
||||
|
||||
// issue a restart command by shelling out to our control script in a detached child
|
||||
child = cp.spawn( "bin/control.sh", ["restart"], {
|
||||
detached: true,
|
||||
stdio: ['ignore', 'ignore', 'ignore']
|
||||
} );
|
||||
child.unref();
|
||||
},
|
||||
|
||||
shutdownCluster: function() {
|
||||
// shut down all server connections
|
||||
if (this.sockets) {
|
||||
for (var id in this.sockets) {
|
||||
var socket = this.sockets[id];
|
||||
this.logDebug(9, "Closing client socket: " + socket.id);
|
||||
socket.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.multi.master) {
|
||||
for (var hostname in this.slaves) {
|
||||
var slave = this.slaves[hostname];
|
||||
if (slave.socket) {
|
||||
this.logDebug(9, "Closing slave connection: " + slave.hostname, slave.socket.id);
|
||||
slave.socket._pixl_disconnected = true;
|
||||
slave.socket.off('disconnect');
|
||||
slave.socket.disconnect();
|
||||
delete slave.socket;
|
||||
}
|
||||
if (slave.socketReconnectTimer) {
|
||||
clearTimeout( slave.socketReconnectTimer );
|
||||
delete slave.socketReconnectTimer;
|
||||
}
|
||||
}
|
||||
this.slaves = {};
|
||||
} // master
|
||||
}
|
||||
|
||||
});
|
180
workspaces/base-workspace/Cronicle-0.8.61/lib/discovery.js
Executable file
|
@ -0,0 +1,180 @@
|
|||
// Cronicle Server Discovery Layer
|
||||
// Copyright (c) 2015 Joseph Huckaby
|
||||
// Released under the MIT License
|
||||
|
||||
var dgram = require("dgram");
|
||||
var os = require('os');
|
||||
var Netmask = require('netmask').Netmask;
|
||||
|
||||
var Class = require("pixl-class");
|
||||
var Tools = require("pixl-tools");
|
||||
|
||||
module.exports = Class.create({
|
||||
|
||||
nearbyServers: null,
|
||||
lastDiscoveryBroadcast: 0,
|
||||
|
||||
setupDiscovery: function(callback) {
|
||||
// setup auto-discovery system
|
||||
// listen for UDP pings, and broadcast our own ping
|
||||
var self = this;
|
||||
|
||||
this.nearbyServers = {};
|
||||
this.lastDiscoveryBroadcast = 0;
|
||||
|
||||
// disable if port is unset
|
||||
if (!this.server.config.get('udp_broadcast_port')) {
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// guess best broadcast IP
|
||||
this.broadcastIP = this.server.config.get('broadcast_ip') || this.calcBroadcastIP();
|
||||
this.logDebug(4, "Using broadcast IP: " + this.broadcastIP );
|
||||
|
||||
// start UDP socket listener
|
||||
this.logDebug(4, "Starting UDP server on port: " + this.server.config.get('udp_broadcast_port'));
|
||||
var listener = this.discoveryListener = dgram.createSocket("udp4");
|
||||
|
||||
listener.on("message", function (msg, rinfo) {
|
||||
self.discoveryReceive( msg, rinfo );
|
||||
} );
|
||||
|
||||
listener.on("error", function (err) {
|
||||
self.logError('udp', "UDP socket listener error: " + err);
|
||||
self.discoveryListener = null;
|
||||
} );
|
||||
|
||||
listener.bind( this.server.config.get('udp_broadcast_port'), function() {
|
||||
if (callback) callback();
|
||||
} );
|
||||
},
|
||||
|
||||
discoveryTick: function() {
|
||||
// broadcast pings every N
|
||||
if (!this.discoveryListener) return;
|
||||
var now = Tools.timeNow(true);
|
||||
|
||||
if (now - this.lastDiscoveryBroadcast >= this.server.config.get('master_ping_freq')) {
|
||||
this.lastDiscoveryBroadcast = now;
|
||||
|
||||
// only broadcast if not part of a cluster
|
||||
if (!this.multi.cluster) {
|
||||
this.discoveryBroadcast( 'heartbeat', {
|
||||
hostname: this.server.hostname,
|
||||
ip: this.server.ip
|
||||
} );
|
||||
}
|
||||
|
||||
// prune servers who have stopped broadcasting
|
||||
for (var hostname in this.nearbyServers) {
|
||||
var server = this.nearbyServers[hostname];
|
||||
if (now - server.now >= this.server.config.get('master_ping_timeout')) {
|
||||
delete this.nearbyServers[hostname];
|
||||
if (this.multi.master) {
|
||||
this.authSocketEmit( 'update', { nearby: this.nearbyServers } );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
discoveryBroadcast: function(type, message, callback) {
|
||||
// broadcast message via UDP
|
||||
var self = this;
|
||||
|
||||
message.action = type;
|
||||
this.logDebug(10, "Broadcasting message: " + type, message);
|
||||
|
||||
var client = dgram.createSocket('udp4');
|
||||
var message = Buffer.from( JSON.stringify(message) + "\n" );
|
||||
client.bind( 0, function() {
|
||||
client.setBroadcast( true );
|
||||
client.send(message, 0, message.length, self.server.config.get('udp_broadcast_port'), self.broadcastIP, function(err) {
|
||||
if (err) self.logDebug(9, "UDP broadcast failed: " + err);
|
||||
client.close();
|
||||
if (callback) callback();
|
||||
} );
|
||||
} );
|
||||
},
|
||||
|
||||
discoveryReceive: function(msg, rinfo) {
|
||||
// receive UDP message from another server
|
||||
this.logDebug(10, "Received UDP message: " + msg + " from " + rinfo.address + ":" + rinfo.port);
|
||||
|
||||
var text = msg.toString();
|
||||
if (text.match(/^\{/)) {
|
||||
// appears to be JSON
|
||||
var json = null;
|
||||
try { json = JSON.parse(text); }
|
||||
catch (e) {
|
||||
this.logError(9, "Failed to parse UDP JSON message: " + e);
|
||||
}
|
||||
if (json && json.action) {
|
||||
switch (json.action) {
|
||||
|
||||
case 'heartbeat':
|
||||
if (json.hostname && (json.hostname != this.server.hostname)) {
|
||||
json.now = Tools.timeNow();
|
||||
delete json.action;
|
||||
|
||||
if (!this.nearbyServers[ json.hostname ]) {
|
||||
// first time we've seen this server
|
||||
this.nearbyServers[ json.hostname ] = json;
|
||||
if (this.multi.master) {
|
||||
this.logDebug(6, "Discovered nearby server: " + json.hostname, json);
|
||||
this.authSocketEmit( 'update', { nearby: this.nearbyServers } );
|
||||
}
|
||||
}
|
||||
else {
|
||||
// update from existing server
|
||||
this.nearbyServers[ json.hostname ] = json;
|
||||
}
|
||||
this.logDebug(10, "Received heartbeat from: " + json.hostname, json);
|
||||
}
|
||||
break;
|
||||
|
||||
} // switch action
|
||||
} // got json
|
||||
} // appears to be json
|
||||
},
|
||||
|
||||
calcBroadcastIP: function() {
|
||||
// Attempt to determine server's Broadcast IP, using the first LAN IP and Netmask
|
||||
// https://en.wikipedia.org/wiki/Broadcast_address
|
||||
var ifaces = os.networkInterfaces();
|
||||
var addrs = [];
|
||||
for (var key in ifaces) {
|
||||
if (ifaces[key] && ifaces[key].length) {
|
||||
Array.from(ifaces[key]).forEach( function(item) { addrs.push(item); } );
|
||||
}
|
||||
}
|
||||
var addr = Tools.findObject( addrs, { family: 'IPv4', internal: false } );
|
||||
if (addr && addr.address && addr.address.match(/^\d+\.\d+\.\d+\.\d+$/) && addr.netmask && addr.netmask.match(/^\d+\.\d+\.\d+\.\d+$/)) {
|
||||
// well that was easy
|
||||
var ip = addr.address;
|
||||
var mask = addr.netmask;
|
||||
|
||||
var block = null;
|
||||
try { block = new Netmask( ip + '/' + mask ); }
|
||||
catch (err) {;}
|
||||
|
||||
if (block && block.broadcast && block.broadcast.match(/^\d+\.\d+\.\d+\.\d+$/)) {
|
||||
return block.broadcast;
|
||||
}
|
||||
}
|
||||
return '255.255.255.255';
|
||||
},
|
||||
|
||||
shutdownDiscovery: function() {
|
||||
// shutdown
|
||||
var self = this;
|
||||
|
||||
// shutdown UDP listener
|
||||
if (this.discoveryListener) {
|
||||
this.logDebug(3, "Shutting down UDP server");
|
||||
this.discoveryListener.close();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
1065
workspaces/base-workspace/Cronicle-0.8.61/lib/engine.js
Executable file
1963
workspaces/base-workspace/Cronicle-0.8.61/lib/job.js
Executable file
38
workspaces/base-workspace/Cronicle-0.8.61/lib/main.js
Executable file
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Cronicle Server - Main entry point
|
||||
// Copyright (c) 2015 - 2018 Joseph Huckaby
|
||||
// Released under the MIT License
|
||||
|
||||
// Emit warning for broken versions of node v10
|
||||
// See: https://github.com/jhuckaby/Cronicle/issues/108
|
||||
if (process.version.match(/^v10\.[012345678]\.\d+$/)) {
|
||||
console.error("\nWARNING: You are using an incompatible version of Node.js (" + process.version + ") with a known timer bug.\nCronicle will stop working after approximately 25 days under these conditions.\nIt is highly recommended that you upgrade to Node.js v10.9.0 or later, or downgrade to Node LTS (v8.x).\nSee https://github.com/jhuckaby/Cronicle/issues/108 for details.\n");
|
||||
}
|
||||
|
||||
var PixlServer = require("pixl-server");
|
||||
|
||||
// chdir to the proper server root dir
|
||||
process.chdir( require('path').dirname( __dirname ) );
|
||||
|
||||
var server = new PixlServer({
|
||||
|
||||
__name: 'Cronicle',
|
||||
__version: require('../package.json').version,
|
||||
|
||||
configFile: "conf/config.json",
|
||||
|
||||
components: [
|
||||
require('pixl-server-storage'),
|
||||
require('pixl-server-web'),
|
||||
require('pixl-server-api'),
|
||||
require('pixl-server-user'),
|
||||
require('./engine.js')
|
||||
]
|
||||
|
||||
});
|
||||
|
||||
server.startup( function() {
|
||||
// server startup complete
|
||||
process.title = server.__name + ' Server';
|
||||
} );
|
168
workspaces/base-workspace/Cronicle-0.8.61/lib/queue.js
Executable file
|
@ -0,0 +1,168 @@
|
|||
// Cronicle Server Queue Layer
|
||||
// Copyright (c) 2015 Joseph Huckaby
|
||||
// Released under the MIT License
|
||||
|
||||
// Note: Special queue task properties are 'action' and 'when'.
|
||||
// These are meta properties, and are DELETED when the task is executed.
|
||||
|
||||
var fs = require("fs");
|
||||
var async = require('async');
|
||||
var glob = require('glob');
|
||||
|
||||
var Class = require("pixl-class");
|
||||
var Tools = require("pixl-tools");
|
||||
|
||||
module.exports = Class.create({
|
||||
|
||||
internalQueue: null,
|
||||
|
||||
setupQueue: function() {
|
||||
// setup queue system
|
||||
if (!this.internalQueue) {
|
||||
this.internalQueue = {};
|
||||
|
||||
// check in-memory queue every second
|
||||
this.server.on('tick', this.monitorInternalQueue.bind(this));
|
||||
|
||||
// check external queue every minute
|
||||
this.server.on('minute', this.monitorExternalQueue.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
monitorInternalQueue: function() {
|
||||
// monitor in-memory queue for tasks ready to execute
|
||||
// these may be future-scheduled via 'when' property
|
||||
// (this is called once per second)
|
||||
var now = Tools.timeNow();
|
||||
|
||||
// don't run this if shutting down
|
||||
if (this.server.shut) return;
|
||||
|
||||
for (var key in this.internalQueue) {
|
||||
var task = this.internalQueue[key];
|
||||
if (!task.when || (now >= task.when)) {
|
||||
// invoke method from 'action' property
|
||||
this.logDebug(5, "Processing internal queue task", task);
|
||||
var action = task.action || 'UNKNOWN';
|
||||
delete task.action;
|
||||
delete task.when;
|
||||
|
||||
if (this[action]) {
|
||||
this[action](task);
|
||||
}
|
||||
else {
|
||||
this.logError('queue', "Unsupported action: " + action, task);
|
||||
}
|
||||
delete this.internalQueue[key];
|
||||
} // execute
|
||||
} // foreach item
|
||||
},
|
||||
|
||||
enqueueInternal: function(task) {
|
||||
// enqueue task into internal queue
|
||||
var key = Tools.generateUniqueID(32);
|
||||
this.logDebug(9, "Enqueuing internal task: " + key, task);
|
||||
this.internalQueue[key] = task;
|
||||
},
|
||||
|
||||
monitorExternalQueue: function() {
|
||||
// monitor queue dir for files (called once per minute)
|
||||
var self = this;
|
||||
var file_spec = this.server.config.get('queue_dir') + '/*.json';
|
||||
|
||||
// don't run this if shutting down
|
||||
if (this.server.shut) return;
|
||||
|
||||
glob(file_spec, {}, function (err, files) {
|
||||
// got task files
|
||||
if (files && files.length) {
|
||||
async.eachSeries( files, function(file, callback) {
|
||||
// foreach task file
|
||||
fs.readFile( file, { encoding: 'utf8' }, function(err, data) {
|
||||
// delete right away, regardless of outcome
|
||||
fs.unlink( file, function(err) {;} );
|
||||
|
||||
// parse json
|
||||
var task = null;
|
||||
try { task = JSON.parse( data ); }
|
||||
catch (err) {
|
||||
self.logError('queue', "Failed to parse queued JSON file: " + file + ": " + err);
|
||||
}
|
||||
|
||||
if (task) {
|
||||
self.logDebug(5, "Processing external queue task: " + file, task);
|
||||
|
||||
if (task.when) {
|
||||
// task is set for a future time, add to internal memory queue
|
||||
self.enqueueInternal(task);
|
||||
}
|
||||
else {
|
||||
// run now, invoke method from 'action' property
|
||||
var action = task.action || 'UNKNOWN';
|
||||
delete task.action;
|
||||
delete task.when;
|
||||
|
||||
if (self[action]) {
|
||||
self[action](task);
|
||||
}
|
||||
else {
|
||||
self.logError('queue', "Unsupported action: " + action, task);
|
||||
}
|
||||
} // run now
|
||||
} // good task
|
||||
|
||||
callback();
|
||||
} );
|
||||
},
|
||||
function(err) {
|
||||
// done with glob eachSeries
|
||||
self.logDebug(9, "No more queue files to process");
|
||||
} );
|
||||
} // got files
|
||||
} ); // glob
|
||||
},
|
||||
|
||||
enqueueExternal: function(task, callback) {
|
||||
// enqueue a task for later (up to 1 minute delay)
|
||||
// these may be future-scheduled via 'when' property
|
||||
var self = this;
|
||||
var task_file = this.server.config.get('queue_dir') + '/' + Tools.generateUniqueID(32) + '.json';
|
||||
var temp_file = task_file + '.tmp';
|
||||
|
||||
this.logDebug(9, "Enqueuing external task", task);
|
||||
|
||||
fs.writeFile( temp_file, JSON.stringify(task), function(err) {
|
||||
if (err) {
|
||||
self.logError('queue', "Failed to write queue file: " + temp_file + ": " + err);
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
fs.rename( temp_file, task_file, function(err) {
|
||||
if (err) {
|
||||
self.logError('queue', "Failed to rename queue file: " + temp_file + ": " + task_file + ": " + err);
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (callback) callback();
|
||||
} ); // rename
|
||||
} ); // writeFile
|
||||
},
|
||||
|
||||
shutdownQueue: function() {
|
||||
// shut down queues
|
||||
// move internal pending queue items to external queue
|
||||
// to be picked up on next startup
|
||||
var self = this;
|
||||
|
||||
if (this.internalQueue) {
|
||||
for (var key in this.internalQueue) {
|
||||
var task = this.internalQueue[key];
|
||||
this.enqueueExternal( task );
|
||||
}
|
||||
this.internalQueue = null;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
505
workspaces/base-workspace/Cronicle-0.8.61/lib/scheduler.js
Executable file
|
@ -0,0 +1,505 @@
|
|||
// Cronicle Server Scheduler
|
||||
// Copyright (c) 2015 Joseph Huckaby
|
||||
// Released under the MIT License
|
||||
|
||||
var async = require('async');
|
||||
var fs = require('fs');
|
||||
var moment = require('moment-timezone');
|
||||
|
||||
var Class = require("pixl-class");
|
||||
var Tools = require("pixl-tools");
|
||||
var PixlMail = require('pixl-mail');
|
||||
|
||||
module.exports = Class.create({
|
||||
|
||||
setupScheduler: function() {
|
||||
// load previous event cursors
|
||||
var self = this;
|
||||
var now = Tools.normalizeTime( Tools.timeNow(), { sec: 0 } );
|
||||
|
||||
this.storage.get( 'global/state', function(err, state) {
|
||||
if (!err && state) self.state = state;
|
||||
var cursors = self.state.cursors;
|
||||
|
||||
// if running in debug mode, clear stats
|
||||
if (self.server.debug) self.state.stats = {};
|
||||
|
||||
self.storage.listGet( 'global/schedule', 0, 0, function(err, items) {
|
||||
// got all schedule items
|
||||
var queue_event_ids = [];
|
||||
|
||||
for (var idx = 0, len = items.length; idx < len; idx++) {
|
||||
var item = items[idx];
|
||||
|
||||
// reset cursor to now if running in debug mode, or event is NOT set to catch up
|
||||
if (self.server.debug || !item.catch_up) {
|
||||
cursors[ item.id ] = now;
|
||||
}
|
||||
|
||||
// if event has queue, add to load list
|
||||
if (item.queue) queue_event_ids.push( item.id );
|
||||
} // foreach item
|
||||
|
||||
// load event queue counts
|
||||
if (queue_event_ids.length) async.eachSeries( queue_event_ids,
|
||||
function(event_id, callback) {
|
||||
self.storage.listGetInfo( 'global/event_queue/' + event_id, function(err, list) {
|
||||
if (!list) list = { length: 0 };
|
||||
self.eventQueue[ event_id ] = list.length || 0;
|
||||
callback();
|
||||
} );
|
||||
}
|
||||
); // eachSeries
|
||||
|
||||
// set a grace period to allow all slaves to check-in before we start launching jobs
|
||||
// (important for calculating max concurrents -- master may have inherited a mess)
|
||||
self.schedulerGraceTimer = setTimeout( function() {
|
||||
delete self.schedulerGraceTimer;
|
||||
|
||||
self.server.on('minute', function(dargs) {
|
||||
self.schedulerMinuteTick(dargs);
|
||||
self.checkAllEventQueues();
|
||||
} );
|
||||
|
||||
// fire up queues if applicable
|
||||
if (queue_event_ids.length) self.checkEventQueues( queue_event_ids );
|
||||
|
||||
}, self.server.config.get('scheduler_startup_grace') * 1000 );
|
||||
|
||||
} ); // loaded schedule
|
||||
} ); // loaded state
|
||||
},
|
||||
|
||||
schedulerMinuteTick: function(dargs, catch_up_only) {
|
||||
// a new minute has started, see if jobs need to run
|
||||
var self = this;
|
||||
var cursors = this.state.cursors;
|
||||
var launches = {};
|
||||
|
||||
// don't run this if shutting down
|
||||
if (this.server.shut) return;
|
||||
|
||||
if (this.state.enabled) {
|
||||
// scheduler is enabled, advance time
|
||||
this.schedulerTicking = true;
|
||||
if (!dargs) dargs = Tools.getDateArgs( Tools.timeNow(true) );
|
||||
|
||||
dargs.sec = 0; // normalize seconds
|
||||
var now = Tools.getTimeFromArgs(dargs);
|
||||
|
||||
if (catch_up_only) {
|
||||
self.logDebug(4, "Scheduler catching events up to: " + dargs.yyyy_mm_dd + " " + dargs.hh + ":" + dargs.mi + ":00" );
|
||||
}
|
||||
else {
|
||||
self.logDebug(4, "Scheduler Minute Tick: Advancing time up to: " + dargs.yyyy_mm_dd + " " + dargs.hh + ":" + dargs.mi + ":00" );
|
||||
}
|
||||
|
||||
self.storage.listGet( 'global/schedule', 0, 0, function(err, items) {
|
||||
// got all schedule items, step through them in series
|
||||
if (err) {
|
||||
self.logError('storage', "Failed to fetch schedule: " + err);
|
||||
items = [];
|
||||
}
|
||||
|
||||
async.eachSeries( items, async.ensureAsync( function(item, callback) {
|
||||
if (!item.enabled) {
|
||||
// item is disabled, skip over entirely
|
||||
// for catch_up events, this means jobs will 'accumulate'
|
||||
return callback();
|
||||
}
|
||||
if (!item.catch_up) {
|
||||
// no catch up needed, so only process current minute
|
||||
if (catch_up_only) {
|
||||
return callback();
|
||||
}
|
||||
cursors[ item.id ] = now - 60;
|
||||
}
|
||||
var cursor = cursors[ item.id ];
|
||||
|
||||
// now step over each minute we missed
|
||||
async.whilst(
|
||||
function () { return cursor < now; },
|
||||
|
||||
async.ensureAsync( function (callback) {
|
||||
cursor += 60;
|
||||
// var cargs = Tools.getDateArgs(cursor);
|
||||
var margs = moment.tz(cursor * 1000, item.timezone || self.tz);
|
||||
|
||||
if (item.timing && self.checkEventTimingMoment(item.timing, margs)) {
|
||||
// item needs to run!
|
||||
self.logDebug(4, "Auto-launching scheduled item: " + item.id + " (" + item.title + ") for timestamp: " + margs.format('llll z') );
|
||||
self.launchOrQueueJob( Tools.mergeHashes(item, { now: cursor }), callback );
|
||||
}
|
||||
else callback();
|
||||
} ),
|
||||
|
||||
function (err) {
|
||||
if (err) {
|
||||
var err_msg = "Failed to launch scheduled event: " + item.title + ": " + (err.message || err);
|
||||
self.logError('scheduler', err_msg);
|
||||
|
||||
// only log visible error if not in catch_up_only mode, and cursor is near current time
|
||||
if (!catch_up_only && (Tools.timeNow(true) - cursor <= 30) && !err_msg.match(/(Category|Plugin).+\s+is\s+disabled\b/) && !launches[item.id]) {
|
||||
self.logActivity( 'warning', { description: err_msg } );
|
||||
if (item.notify_fail) {
|
||||
self.sendEventErrorEmail( item, { description: err_msg } );
|
||||
}
|
||||
|
||||
var hook_data = Tools.mergeHashes( item, {
|
||||
action: 'job_launch_failure',
|
||||
code: 1,
|
||||
description: (err.message || err),
|
||||
event: item.id,
|
||||
event_title: item.title
|
||||
} );
|
||||
|
||||
// prepare nice text summary (compatible with Slack Incoming WebHooks)
|
||||
hook_data.base_app_url = self.server.config.get('base_app_url');
|
||||
hook_data.edit_event_url = self.server.config.get('base_app_url') + '/#Schedule?sub=edit_event&id=' + item.id;
|
||||
|
||||
var hook_text_templates = self.server.config.get('web_hook_text_templates') || self.defaultWebHookTextTemplates;
|
||||
|
||||
if (hook_text_templates[hook_data.action]) {
|
||||
hook_data.text = Tools.sub( hook_text_templates[hook_data.action], hook_data );
|
||||
|
||||
// include web_hook_config_keys if configured
|
||||
if (self.server.config.get('web_hook_config_keys')) {
|
||||
var web_hook_config_keys = self.server.config.get('web_hook_config_keys');
|
||||
for (var idy = 0, ley = web_hook_config_keys.length; idy < ley; idy++) {
|
||||
var key = web_hook_config_keys[idy];
|
||||
hook_data[key] = self.server.config.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
// include web_hook_custom_data if configured
|
||||
if (self.server.config.get('web_hook_custom_data')) {
|
||||
var web_hook_custom_data = self.server.config.get('web_hook_custom_data');
|
||||
for (var key in web_hook_custom_data) hook_data[key] = web_hook_custom_data[key];
|
||||
}
|
||||
|
||||
// custom http options for web hook
|
||||
var hook_opts = self.server.config.get('web_hook_custom_opts') || {};
|
||||
|
||||
if (item.web_hook) {
|
||||
self.logDebug(9, "Firing web hook for job launch failure: " + item.web_hook);
|
||||
self.request.json( item.web_hook, hook_data, hook_opts, function(err, resp, data) {
|
||||
// log response
|
||||
if (err) self.logDebug(9, "Web Hook Error: " + item.web_hook + ": " + err);
|
||||
else self.logDebug(9, "Web Hook Response: " + item.web_hook + ": HTTP " + resp.statusCode + " " + resp.statusMessage);
|
||||
} );
|
||||
}
|
||||
if (self.server.config.get('universal_web_hook')) {
|
||||
self.logDebug(9, "Firing universal web hook for job launch failure: " + self.server.config.get('universal_web_hook'));
|
||||
self.request.json( self.server.config.get('universal_web_hook'), hook_data, hook_opts, function(err, resp, data) {
|
||||
// log response
|
||||
if (err) self.logDebug(9, "Universal Web Hook Error: " + err);
|
||||
else self.logDebug(9, "Universal Web Hook Response: HTTP " + resp.statusCode + " " + resp.statusMessage);
|
||||
} );
|
||||
} // universal_web_hook
|
||||
} // yes fire hook
|
||||
|
||||
// update failed job count for the day
|
||||
var stats = self.state.stats;
|
||||
if (!stats.jobs_failed) stats.jobs_failed = 1;
|
||||
else stats.jobs_failed++;
|
||||
} // notify for error
|
||||
|
||||
cursor -= 60; // backtrack if we misfired
|
||||
} // error
|
||||
else {
|
||||
launches[ item.id ] = 1;
|
||||
}
|
||||
|
||||
cursors[ item.id ] = cursor;
|
||||
callback();
|
||||
}
|
||||
); // whilst
|
||||
} ),
|
||||
function(err) {
|
||||
// error should never occur here, but just in case
|
||||
if (err) self.logError('scheduler', "Failed to iterate schedule: " + err);
|
||||
|
||||
// all items complete, save new cursor positions back to storage
|
||||
self.storage.put( 'global/state', self.state, function(err) {
|
||||
if (err) self.logError('state', "Failed to update state: " + err);
|
||||
} );
|
||||
|
||||
// send state data to all web clients
|
||||
self.authSocketEmit( 'update', { state: self.state } );
|
||||
|
||||
// remove in-use flag
|
||||
self.schedulerTicking = false;
|
||||
} ); // foreach item
|
||||
} ); // loaded schedule
|
||||
} // scheduler enabled
|
||||
else {
|
||||
// scheduler disabled, but still send state event every minute
|
||||
self.authSocketEmit( 'update', { state: self.state } );
|
||||
}
|
||||
},
|
||||
|
||||
checkEventTiming: function(timing, cursor, tz) {
|
||||
// check if event needs to run
|
||||
if (!timing) return false;
|
||||
var margs = moment.tz(cursor * 1000, tz || this.tz);
|
||||
return this.checkEventTimingMoment(timing, margs);
|
||||
},
|
||||
|
||||
checkEventTimingMoment: function(timing, margs) {
|
||||
// check if event needs to run using Moment.js API
|
||||
if (!timing) return false;
|
||||
if (timing.minutes && timing.minutes.length && (timing.minutes.indexOf(margs.minute()) == -1)) return false;
|
||||
if (timing.hours && timing.hours.length && (timing.hours.indexOf(margs.hour()) == -1)) return false;
|
||||
if (timing.weekdays && timing.weekdays.length && (timing.weekdays.indexOf(margs.day()) == -1)) return false;
|
||||
if (timing.days && timing.days.length && (timing.days.indexOf(margs.date()) == -1)) return false;
|
||||
if (timing.months && timing.months.length && (timing.months.indexOf(margs.month() + 1) == -1)) return false;
|
||||
if (timing.years && timing.years.length && (timing.years.indexOf(margs.year()) == -1)) return false;
|
||||
return true;
|
||||
},
|
||||
|
||||
sendEventErrorEmail: function(event, overrides) {
|
||||
// send general error e-mail for event (i.e. failed to launch)
|
||||
var self = this;
|
||||
var email_template = "conf/emails/event_error.txt";
|
||||
var to = event.notify_fail;
|
||||
var dargs = Tools.getDateArgs( Tools.timeNow() );
|
||||
var email_data = Tools.mergeHashes(event, overrides || {});
|
||||
|
||||
email_data.env = process.env;
|
||||
email_data.config = this.server.config.get();
|
||||
email_data.edit_event_url = this.server.config.get('base_app_url') + '/#Schedule?sub=edit_event&id=' + event.id;
|
||||
email_data.nice_date_time = dargs.yyyy_mm_dd + ' ' + dargs.hh_mi_ss + ' (' + dargs.tz + ')';
|
||||
email_data.description = (email_data.description || '(No description provided)').trim();
|
||||
email_data.notes = (email_data.notes || '(None)').trim();
|
||||
email_data.hostname = this.server.hostname;
|
||||
|
||||
// construct mailer
|
||||
var mail = new PixlMail( this.server.config.get('smtp_hostname'), this.server.config.get('smtp_port') || 25 );
|
||||
mail.setOptions( this.server.config.get('mail_options') || {} );
|
||||
|
||||
// send it
|
||||
mail.send( email_template, email_data, function(err, raw_email) {
|
||||
if (err) {
|
||||
var err_msg = "Failed to send e-mail for event: " + event.id + ": " + to + ": " + err;
|
||||
self.logError( 'mail', err_msg, { text: raw_email } );
|
||||
self.logActivity( 'error', { description: err_msg } );
|
||||
}
|
||||
else {
|
||||
self.logDebug(5, "Email sent successfully for event: " + event.id, { text: raw_email } );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
chainReaction: function(old_job, chain_event_id) {
|
||||
// launch custom new job from completed one
|
||||
var self = this;
|
||||
|
||||
this.storage.listFind( 'global/schedule', { id: chain_event_id }, function(err, event) {
|
||||
if (err || !event) {
|
||||
var err_msg = "Failed to launch chain reaction: Event ID not found: " + chain_event_id;
|
||||
self.logError('scheduler', err_msg);
|
||||
self.logActivity( 'warning', { description: err_msg } );
|
||||
if (old_job.notify_fail) {
|
||||
self.sendEventErrorEmail( old_job, { description: err_msg } );
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var job = Tools.mergeHashes( Tools.copyHash(event, true), {
|
||||
chain_data: old_job.chain_data || {},
|
||||
chain_code: old_job.code || 0,
|
||||
chain_description: old_job.description || '',
|
||||
source: "Chain Reaction (" + old_job.event_title + ")",
|
||||
source_event: old_job.event
|
||||
} );
|
||||
|
||||
self.logDebug(6, "Running event via chain reaction: " + job.title, job);
|
||||
|
||||
self.launchOrQueueJob( job, function(err, jobs_launched) {
|
||||
if (err) {
|
||||
var err_msg = "Failed to launch chain reaction: " + job.title + ": " + err.message;
|
||||
self.logError('scheduler', err_msg);
|
||||
self.logActivity( 'warning', { description: err_msg } );
|
||||
if (job.notify_fail) {
|
||||
self.sendEventErrorEmail( job, { description: err_msg } );
|
||||
}
|
||||
else if (old_job.notify_fail) {
|
||||
self.sendEventErrorEmail( old_job, { description: err_msg } );
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// multiple jobs may have been launched (multiplex)
|
||||
for (var idx = 0, len = jobs_launched.length; idx < len; idx++) {
|
||||
var job_temp = jobs_launched[idx];
|
||||
var stub = { id: job_temp.id, event: job_temp.event, chain_reaction: 1, source_event: old_job.event };
|
||||
self.logTransaction('job_run', job_temp.event_title, stub);
|
||||
}
|
||||
|
||||
} ); // launch job
|
||||
} ); // find event
|
||||
},
|
||||
|
||||
checkAllEventQueues: function(callback) {
|
||||
// check event queues for ALL events
|
||||
var self = this;
|
||||
|
||||
// don't run this if shutting down
|
||||
if (this.server.shut) {
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// must be master to do this
|
||||
if (!this.multi.master) {
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
this.storage.listGet( 'global/schedule', 0, 0, function(err, items) {
|
||||
if (err || !items) {
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
var queue_event_ids = [];
|
||||
|
||||
for (var idx = 0, len = items.length; idx < len; idx++) {
|
||||
var item = items[idx];
|
||||
if (item.queue) queue_event_ids.push( item.id );
|
||||
} // foreach item
|
||||
|
||||
if (queue_event_ids.length) {
|
||||
self.checkEventQueues( queue_event_ids, callback );
|
||||
}
|
||||
else {
|
||||
if (callback) callback();
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
checkEventQueues: function(event_ids, callback) {
|
||||
// check event queues for specific list of event IDs,
|
||||
// and run events if possible
|
||||
var self = this;
|
||||
this.logDebug(9, "Checking event queues", event_ids);
|
||||
|
||||
// don't run this if shutting down
|
||||
if (this.server.shut) {
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// must be master to do this
|
||||
if (!this.multi.master) {
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(event_ids)) event_ids = [ event_ids ];
|
||||
var hot_event_ids = [];
|
||||
|
||||
// only consider events with items in the queue
|
||||
event_ids.forEach( function(event_id) {
|
||||
if (self.eventQueue[event_id]) hot_event_ids.push( event_id );
|
||||
} ); // forEach
|
||||
|
||||
async.eachSeries( hot_event_ids,
|
||||
function(event_id, callback) {
|
||||
// load first item from queue
|
||||
var list_path = 'global/event_queue/' + event_id;
|
||||
self.logDebug(9, "Attempting to dequeue job from event queue: " + event_id);
|
||||
|
||||
self.storage.lock( list_path, true, function() {
|
||||
// locked
|
||||
self.storage.listGet( list_path, 0, 1, function(err, events) {
|
||||
if (err || !events || !events[0]) {
|
||||
self.storage.unlock( list_path );
|
||||
return callback();
|
||||
}
|
||||
var event = events[0];
|
||||
|
||||
// try to launch (without auto-queue), and catch error before anything is logged
|
||||
self.launchJob( event, function(err, jobs_launched) {
|
||||
if (err) {
|
||||
// no problem, job cannot launch at this time
|
||||
self.logDebug(9, "Job dequeue launch failed, item will remain in queue", {
|
||||
err: '' + err,
|
||||
event: event_id
|
||||
});
|
||||
self.storage.unlock( list_path );
|
||||
return callback();
|
||||
}
|
||||
|
||||
self.logDebug(9, "Queue launch successful!", { event: event_id });
|
||||
|
||||
// we queue-launched! decrement counter and shift from list
|
||||
if (self.eventQueue[event_id]) self.eventQueue[event_id]--;
|
||||
self.authSocketEmit( 'update', { eventQueue: self.eventQueue } );
|
||||
|
||||
self.storage.listShift( list_path, function(err) {
|
||||
if (err) self.logDebug(3, "Failed to shift queue: " + err);
|
||||
self.storage.unlock( list_path );
|
||||
callback();
|
||||
} );
|
||||
|
||||
// multiple jobs may have been launched (multiplex)
|
||||
for (var idx = 0, len = jobs_launched.length; idx < len; idx++) {
|
||||
var job_temp = jobs_launched[idx];
|
||||
var stub = { id: job_temp.id, event: job_temp.event, dequeued: 1 };
|
||||
self.logTransaction('job_run', job_temp.event_title, stub);
|
||||
}
|
||||
} ); // launchJob
|
||||
} ); // listGet
|
||||
} ); // lock
|
||||
},
|
||||
function() {
|
||||
if (callback) callback();
|
||||
}
|
||||
); // eachSeries
|
||||
},
|
||||
|
||||
deleteEventQueues: function(event_ids, callback) {
|
||||
// delete one or more event queues
|
||||
var self = this;
|
||||
if (!Array.isArray(event_ids)) event_ids = [ event_ids ];
|
||||
|
||||
async.eachSeries( event_ids,
|
||||
function(event_id, callback) {
|
||||
// remove count from RAM, then delete storage list
|
||||
self.logDebug(4, "Deleting event queue: " + event_id);
|
||||
|
||||
delete self.eventQueue[event_id];
|
||||
|
||||
self.storage.listDelete( 'global/event_queue/' + event_id, true, function(err) {
|
||||
// ignore error, as list may not exist
|
||||
callback();
|
||||
} );
|
||||
},
|
||||
function() {
|
||||
// send eventQueue update to connected clients
|
||||
self.authSocketEmit( 'update', { eventQueue: self.eventQueue } );
|
||||
if (callback) callback();
|
||||
}
|
||||
); // eachSeries
|
||||
},
|
||||
|
||||
shutdownScheduler: function(callback) {
|
||||
// persist state to storage
|
||||
var self = this;
|
||||
if (!this.multi.master) {
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.schedulerGraceTimer) {
|
||||
clearTimeout( this.schedulerGraceTimer );
|
||||
delete this.schedulerGraceTimer;
|
||||
}
|
||||
|
||||
this.storage.put( 'global/state', this.state, function(err) {
|
||||
if (err) self.logError('state', "Failed to update state: " + err);
|
||||
if (callback) callback();
|
||||
} );
|
||||
}
|
||||
|
||||
} );
|
1559
workspaces/base-workspace/Cronicle-0.8.61/lib/test.js
Executable file
63
workspaces/base-workspace/Cronicle-0.8.61/package.json
Executable file
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"name": "Cronicle",
|
||||
"version": "0.8.61",
|
||||
"description": "A simple, distributed task scheduler and runner with a web based UI.",
|
||||
"author": "Joseph Huckaby <jhuckaby@gmail.com>",
|
||||
"homepage": "https://github.com/jhuckaby/Cronicle",
|
||||
"license": "MIT",
|
||||
"main": "lib/main.js",
|
||||
"scripts": {
|
||||
"test": "pixl-unit lib/test.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jhuckaby/Cronicle"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/jhuckaby/Cronicle/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"cron",
|
||||
"crontab",
|
||||
"scheduler"
|
||||
],
|
||||
"dependencies": {
|
||||
"async": "2.6.0",
|
||||
"socket.io": "1.7.3",
|
||||
"socket.io-client": "1.7.3",
|
||||
"mkdirp": "0.5.1",
|
||||
"glob": "5.0.15",
|
||||
"uglify-js": "2.8.22",
|
||||
"zxcvbn": "3.5.0",
|
||||
"jquery": "3.5.0",
|
||||
"font-awesome": "4.7.0",
|
||||
"mdi": "1.9.33",
|
||||
"chart.js": "2.9.4",
|
||||
"moment": "2.22.1",
|
||||
"moment-timezone": "0.5.32",
|
||||
"jstimezonedetect": "1.0.6",
|
||||
"netmask": "2.0.1",
|
||||
"shell-quote": "1.6.1",
|
||||
"bcrypt-node": "0.1.0",
|
||||
"uncatch": "1.0.0",
|
||||
"pixl-args": "1.0.3",
|
||||
"pixl-cli": "1.0.8",
|
||||
"pixl-config": "1.0.5",
|
||||
"pixl-webapp": "1.0.17",
|
||||
"pixl-class": "1.0.2",
|
||||
"pixl-tools": "1.0.23",
|
||||
"pixl-logger": "1.0.14",
|
||||
"pixl-json-stream": "1.0.6",
|
||||
"pixl-request": "1.0.36",
|
||||
"pixl-mail": "1.0.11",
|
||||
"pixl-perf": "1.0.5",
|
||||
"pixl-server": "1.0.26",
|
||||
"pixl-server-storage": "2.0.10",
|
||||
"pixl-server-web": "1.1.7",
|
||||
"pixl-server-api": "1.0.2",
|
||||
"pixl-server-user": "1.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"pixl-unit": "1.0.10"
|
||||
}
|
||||
}
|
137
workspaces/base-workspace/Cronicle-0.8.61/sample_conf/config.json
Executable file
|
@ -0,0 +1,137 @@
|
|||
{
|
||||
"base_app_url": "http://localhost:3012",
|
||||
"email_from": "jobs@cronicle.com",
|
||||
"smtp_hostname": "localhost",
|
||||
"smtp_port": 25,
|
||||
"secret_key": "CHANGE_ME",
|
||||
|
||||
"log_dir": "logs",
|
||||
"log_filename": "[component].log",
|
||||
"log_columns": ["hires_epoch", "date", "hostname", "pid", "component", "category", "code", "msg", "data"],
|
||||
"log_archive_path": "logs/archives/[yyyy]/[mm]/[dd]/[filename]-[yyyy]-[mm]-[dd].log.gz",
|
||||
"log_crashes": true,
|
||||
"copy_job_logs_to": "",
|
||||
"queue_dir": "queue",
|
||||
"pid_file": "logs/cronicled.pid",
|
||||
"debug_level": 9,
|
||||
"maintenance": "04:00",
|
||||
"list_row_max": 10000,
|
||||
"job_data_expire_days": 180,
|
||||
"child_kill_timeout": 10,
|
||||
"dead_job_timeout": 120,
|
||||
"master_ping_freq": 20,
|
||||
"master_ping_timeout": 60,
|
||||
"udp_broadcast_port": 3014,
|
||||
"scheduler_startup_grace": 10,
|
||||
"universal_web_hook": "",
|
||||
"track_manual_jobs": false,
|
||||
"max_jobs": 0,
|
||||
|
||||
"server_comm_use_hostnames": false,
|
||||
"web_direct_connect": false,
|
||||
"web_socket_use_hostnames": false,
|
||||
|
||||
"job_memory_max": 1073741824,
|
||||
"job_memory_sustain": 0,
|
||||
"job_cpu_max": 0,
|
||||
"job_cpu_sustain": 0,
|
||||
"job_log_max_size": 0,
|
||||
"job_env": {},
|
||||
|
||||
"web_hook_text_templates": {
|
||||
"job_start": "Job started on [hostname]: [event_title] [job_details_url]",
|
||||
"job_complete": "Job completed successfully on [hostname]: [event_title] [job_details_url]",
|
||||
"job_failure": "Job failed on [hostname]: [event_title]: Error [code]: [description] [job_details_url]",
|
||||
"job_launch_failure": "Failed to launch scheduled event: [event_title]: [description] [edit_event_url]"
|
||||
},
|
||||
|
||||
"client": {
|
||||
"name": "Cronicle",
|
||||
"debug": 0,
|
||||
"privilege_list": [
|
||||
{ "id": "admin", "title": "Administrator" },
|
||||
{ "id": "create_events", "title": "Create Events" },
|
||||
{ "id": "edit_events", "title": "Edit Events" },
|
||||
{ "id": "delete_events", "title": "Delete Events" },
|
||||
{ "id": "run_events", "title": "Run Events" },
|
||||
{ "id": "abort_events", "title": "Abort Events" },
|
||||
{ "id": "state_update", "title": "Toggle Scheduler" }
|
||||
],
|
||||
"new_event_template": {
|
||||
"enabled": 1,
|
||||
"params": {},
|
||||
"timing": { "minutes": [0] },
|
||||
"max_children": 1,
|
||||
"timeout": 3600,
|
||||
"catch_up": 0,
|
||||
"queue_max": 1000
|
||||
}
|
||||
},
|
||||
|
||||
"Storage": {
|
||||
"engine": "Filesystem",
|
||||
"list_page_size": 50,
|
||||
"concurrency": 4,
|
||||
"log_event_types": { "get": 1, "put": 1, "head": 1, "delete": 1, "expire_set": 1 },
|
||||
|
||||
"Filesystem": {
|
||||
"base_dir": "data",
|
||||
"key_namespaces": 1
|
||||
}
|
||||
},
|
||||
|
||||
"WebServer": {
|
||||
"http_port": 3012,
|
||||
"http_htdocs_dir": "htdocs",
|
||||
"http_max_upload_size": 104857600,
|
||||
"http_static_ttl": 3600,
|
||||
"http_static_index": "index.html",
|
||||
"http_server_signature": "Cronicle 1.0",
|
||||
"http_gzip_text": true,
|
||||
"http_timeout": 30,
|
||||
"http_regex_json": "(text|javascript|js|json)",
|
||||
"http_response_headers": {
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
},
|
||||
|
||||
"https": false,
|
||||
"https_port": 3013,
|
||||
"https_cert_file": "conf/ssl.crt",
|
||||
"https_key_file": "conf/ssl.key",
|
||||
"https_force": false,
|
||||
"https_timeout": 30,
|
||||
"https_header_detect": {
|
||||
"Front-End-Https": "^on$",
|
||||
"X-Url-Scheme": "^https$",
|
||||
"X-Forwarded-Protocol": "^https$",
|
||||
"X-Forwarded-Proto": "^https$",
|
||||
"X-Forwarded-Ssl": "^on$"
|
||||
}
|
||||
},
|
||||
|
||||
"User": {
|
||||
"session_expire_days": 30,
|
||||
"max_failed_logins_per_hour": 5,
|
||||
"max_forgot_passwords_per_hour": 3,
|
||||
"sort_global_users": true,
|
||||
"use_bcrypt": true,
|
||||
|
||||
"email_templates": {
|
||||
"welcome_new_user": "conf/emails/welcome_new_user.txt",
|
||||
"changed_password": "conf/emails/changed_password.txt",
|
||||
"recover_password": "conf/emails/recover_password.txt"
|
||||
},
|
||||
|
||||
"free_accounts": 1,
|
||||
"default_privileges": {
|
||||
"admin": 1,
|
||||
"create_events": 1,
|
||||
"edit_events": 1,
|
||||
"delete_events": 1,
|
||||
"run_events": 1,
|
||||
"abort_events": 1,
|
||||
"state_update": 1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
To: [/user/email]
|
||||
From: [/config/email_from]
|
||||
Subject: Your Cronicle password was changed
|
||||
|
||||
Hey [/user/full_name],
|
||||
|
||||
Someone recently changed the password on your Cronicle account. If this was you, then all is well, and you can disregard this message. However, if you suspect your account is being hacked, you might want to consider using the "Forgot Password" feature (located on the login page) to reset your password.
|
||||
|
||||
Here is the information we gathered from the request:
|
||||
|
||||
Date/Time: [/date_time]
|
||||
IP Address: [/ip]
|
||||
User Agent: [/request/headers/user-agent]
|
||||
|
||||
Regards,
|
||||
The Cronicle Team
|
19
workspaces/base-workspace/Cronicle-0.8.61/sample_conf/emails/event_error.txt
Executable file
|
@ -0,0 +1,19 @@
|
|||
To: [/notify_fail]
|
||||
From: [/config/email_from]
|
||||
Subject: ⚠️ Cronicle Event Error: [/title]
|
||||
|
||||
Date/Time: [/nice_date_time]
|
||||
Event Title: [/title]
|
||||
Hostname: [/hostname]
|
||||
|
||||
Error Description:
|
||||
[/description]
|
||||
|
||||
Edit Event:
|
||||
[/edit_event_url]
|
||||
|
||||
Event Notes:
|
||||
[/notes]
|
||||
|
||||
Regards,
|
||||
The Cronicle Team
|
36
workspaces/base-workspace/Cronicle-0.8.61/sample_conf/emails/job_fail.txt
Executable file
|
@ -0,0 +1,36 @@
|
|||
To: [/notify_fail]
|
||||
From: [/config/email_from]
|
||||
Subject: ⚠️ Cronicle Job Failed: [/event_title]
|
||||
|
||||
Date/Time: [/nice_date_time]
|
||||
Event Title: [/event_title]
|
||||
Category: [/category_title]
|
||||
Server Target: [/nice_target]
|
||||
Plugin: [/plugin_title]
|
||||
|
||||
Job ID: [/id]
|
||||
Hostname: [/hostname]
|
||||
PID: [/pid]
|
||||
Elapsed Time: [/nice_elapsed]
|
||||
Performance Metrics: [/perf]
|
||||
Avg. Memory Usage: [/nice_mem]
|
||||
Avg. CPU Usage: [/nice_cpu]
|
||||
Error Code: [/code]
|
||||
|
||||
Error Description:
|
||||
[/description]
|
||||
|
||||
Job Details:
|
||||
[/job_details_url]
|
||||
|
||||
Job Debug Log ([/nice_log_size]):
|
||||
[/job_log_url]
|
||||
|
||||
Edit Event:
|
||||
[/edit_event_url]
|
||||
|
||||
Event Notes:
|
||||
[/notes]
|
||||
|
||||
Regards,
|
||||
The Cronicle Team
|
35
workspaces/base-workspace/Cronicle-0.8.61/sample_conf/emails/job_success.txt
Executable file
|
@ -0,0 +1,35 @@
|
|||
To: [/notify_success]
|
||||
From: [/config/email_from]
|
||||
Subject: ✅ Cronicle Job Completed Successfully: [/event_title]
|
||||
|
||||
Date/Time: [/nice_date_time]
|
||||
Event Title: [/event_title]
|
||||
Category: [/category_title]
|
||||
Server Target: [/nice_target]
|
||||
Plugin: [/plugin_title]
|
||||
|
||||
Job ID: [/id]
|
||||
Hostname: [/hostname]
|
||||
PID: [/pid]
|
||||
Elapsed Time: [/nice_elapsed]
|
||||
Performance Metrics: [/perf]
|
||||
Avg. Memory Usage: [/nice_mem]
|
||||
Avg. CPU Usage: [/nice_cpu]
|
||||
|
||||
Job Details:
|
||||
[/job_details_url]
|
||||
|
||||
Job Debug Log ([/nice_log_size]):
|
||||
[/job_log_url]
|
||||
|
||||
Edit Event:
|
||||
[/edit_event_url]
|
||||
|
||||
Description:
|
||||
[/description]
|
||||
|
||||
Event Notes:
|
||||
[/notes]
|
||||
|
||||
Regards,
|
||||
The Cronicle Team
|
|
@ -0,0 +1,20 @@
|
|||
To: [/user/email]
|
||||
From: [/config/email_from]
|
||||
Subject: Forgot your Cronicle password?
|
||||
|
||||
Hey [/user/full_name],
|
||||
|
||||
Someone recently requested to have your password reset on your Cronicle account. To make sure this is really you, this confirmation was sent to the e-mail address we have on file for your account. If you really want to reset your password, please click the link below. If you cannot click the link, copy and paste it into your browser.
|
||||
|
||||
[/self_url]#Login?u=[/user/username]&h=[/recovery_key]
|
||||
|
||||
This password reset page will expire after 24 hours.
|
||||
|
||||
If you suspect someone is trying to hack your account, here is the information we gathered from the request:
|
||||
|
||||
Date/Time: [/date_time]
|
||||
IP Address: [/ip]
|
||||
User Agent: [/request/headers/user-agent]
|
||||
|
||||
Regards,
|
||||
The Cronicle Team
|
|
@ -0,0 +1,12 @@
|
|||
To: [/user/email]
|
||||
From: [/config/email_from]
|
||||
Subject: Welcome to Cronicle!
|
||||
|
||||
Hey [/user/full_name],
|
||||
|
||||
Welcome to Cronicle! Your new account username is "[/user/username]". You can login to your new account by clicking the following link, or copying & pasting it into your browser:
|
||||
|
||||
[/self_url]
|
||||
|
||||
Regards,
|
||||
The Cronicle Team
|
174
workspaces/base-workspace/Cronicle-0.8.61/sample_conf/setup.json
Executable file
|
@ -0,0 +1,174 @@
|
|||
{
|
||||
"storage": [
|
||||
[ "put", "users/admin", {
|
||||
"username": "admin",
|
||||
"password": "$2a$10$VAF.FNvz1JqhCAB5rCh9GOa965eYWH3fcgWIuQFAmsZnnVS/.ye1y",
|
||||
"full_name": "Administrator",
|
||||
"email": "admin@cronicle.com",
|
||||
"active": 1,
|
||||
"modified": 1434125333,
|
||||
"created": 1434125333,
|
||||
"salt": "salty",
|
||||
"privileges": {
|
||||
"admin": 1
|
||||
}
|
||||
} ],
|
||||
[ "listCreate", "global/users", { "page_size": 100 } ],
|
||||
[ "listPush", "global/users", { "username": "admin" } ],
|
||||
[ "listCreate", "global/plugins", {} ],
|
||||
[ "listPush", "global/plugins", {
|
||||
"id": "testplug",
|
||||
"title": "Test Plugin",
|
||||
"enabled": 1,
|
||||
"command": "bin/test-plugin.js",
|
||||
"username": "admin",
|
||||
"modified": 1434125333,
|
||||
"created": 1434125333,
|
||||
"params": [
|
||||
{ "id":"duration", "type":"text", "size":10, "title":"Test Duration (seconds)", "value": 60 },
|
||||
{ "id":"progress", "type":"checkbox", "title":"Report Progress", "value": 1 },
|
||||
{ "id":"burn", "type":"checkbox", "title":"Burn Memory/CPU", "value": 0 },
|
||||
{ "id":"action", "type":"select", "title":"Simulate Action", "items":["Success","Failure","Crash"], "value": "Success" },
|
||||
{ "id":"secret", "type":"hidden", "value":"Will not be shown in Event UI" }
|
||||
]
|
||||
} ],
|
||||
[ "listPush", "global/plugins", {
|
||||
"id": "shellplug",
|
||||
"title": "Shell Script",
|
||||
"enabled": 1,
|
||||
"command": "bin/shell-plugin.js",
|
||||
"username": "admin",
|
||||
"modified": 1434125333,
|
||||
"created": 1434125333,
|
||||
"params": [
|
||||
{ "id":"script", "type":"textarea", "rows":10, "title":"Script Source", "value": "#!/bin/sh\n\n# Enter your shell script code here" },
|
||||
{ "id":"annotate", "type":"checkbox", "title":"Add Date/Time Stamps to Log", "value": 0 },
|
||||
{ "id":"json", "type":"checkbox", "title":"Interpret JSON in Output", "value": 0 }
|
||||
]
|
||||
} ],
|
||||
[ "listPush", "global/plugins", {
|
||||
"id": "urlplug",
|
||||
"title": "HTTP Request",
|
||||
"enabled": 1,
|
||||
"command": "bin/url-plugin.js",
|
||||
"username": "admin",
|
||||
"modified": 1434125333,
|
||||
"created": 1434125333,
|
||||
"params": [
|
||||
{ "type":"select", "id":"method", "title":"Method", "items":["GET", "HEAD", "POST"], "value":"GET" },
|
||||
{ "type":"textarea", "id":"url", "title":"URL", "rows":3, "value":"http://" },
|
||||
{ "type":"textarea", "id":"headers", "title":"Request Headers", "rows":4, "value":"User-Agent: Cronicle/1.0" },
|
||||
{ "type":"textarea", "id":"data", "title":"POST Data", "rows":4, "value":"" },
|
||||
{ "type":"text", "id":"timeout", "title":"Timeout (Seconds)", "size":5, "value":"30" },
|
||||
{ "type":"checkbox", "id":"follow", "title":"Follow Redirects", "value":0 },
|
||||
{ "type":"checkbox", "id":"ssl_cert_bypass", "title":"SSL Cert Bypass", "value":0 },
|
||||
{ "type":"text", "id":"success_match", "title":"Success Match", "size":20, "value":"" },
|
||||
{ "type":"text", "id":"error_match", "title":"Error Match", "size":20, "value":"" }
|
||||
]
|
||||
} ],
|
||||
[ "listCreate", "global/categories", {} ],
|
||||
[ "listPush", "global/categories", {
|
||||
"id": "general",
|
||||
"title": "General",
|
||||
"enabled": 1,
|
||||
"username": "admin",
|
||||
"modified": 1434125333,
|
||||
"created": 1434125333,
|
||||
"description": "For events that don't fit anywhere else.",
|
||||
"max_children": 0
|
||||
} ],
|
||||
[ "listCreate", "global/server_groups", {} ],
|
||||
[ "listPush", "global/server_groups", {
|
||||
"id": "maingrp",
|
||||
"title": "Primary Group",
|
||||
"regexp": "_HOSTNAME_",
|
||||
"master": 1
|
||||
} ],
|
||||
[ "listPush", "global/server_groups", {
|
||||
"id": "allgrp",
|
||||
"title": "All Servers",
|
||||
"regexp": ".+",
|
||||
"master": 0
|
||||
} ],
|
||||
[ "listCreate", "global/servers", {} ],
|
||||
[ "listPush", "global/servers", {
|
||||
"hostname": "_HOSTNAME_",
|
||||
"ip": "_IP_"
|
||||
} ],
|
||||
[ "listCreate", "global/schedule", {} ],
|
||||
[ "listCreate", "global/api_keys", {} ]
|
||||
],
|
||||
|
||||
"build": {
|
||||
"common": [
|
||||
[ "symlinkCompress", "node_modules/jquery/dist/jquery.min.js", "htdocs/js/external/" ],
|
||||
[ "symlinkCompress", "node_modules/jquery/dist/jquery.min.map", "htdocs/js/external/" ],
|
||||
[ "symlinkCompress", "node_modules/zxcvbn/dist/zxcvbn.js", "htdocs/js/external/" ],
|
||||
[ "symlinkCompress", "node_modules/zxcvbn/dist/zxcvbn.js.map", "htdocs/js/external/" ],
|
||||
[ "symlinkCompress", "node_modules/chart.js/dist/Chart.min.js", "htdocs/js/external/" ],
|
||||
|
||||
[ "symlinkCompress", "node_modules/font-awesome/css/font-awesome.min.css", "htdocs/css/" ],
|
||||
[ "symlinkCompress", "node_modules/font-awesome/css/font-awesome.css.map", "htdocs/css/" ],
|
||||
[ "copyFiles", "node_modules/font-awesome/fonts/*", "htdocs/fonts/" ],
|
||||
|
||||
[ "symlinkCompress", "node_modules/mdi/css/materialdesignicons.min.css", "htdocs/css/" ],
|
||||
[ "symlinkCompress", "node_modules/mdi/css/materialdesignicons.min.css.map", "htdocs/css/" ],
|
||||
[ "copyFiles", "node_modules/mdi/fonts/*", "htdocs/fonts/" ],
|
||||
|
||||
[ "symlinkCompress", "node_modules/moment/min/moment.min.js", "htdocs/js/external/" ],
|
||||
[ "symlinkCompress", "node_modules/moment-timezone/builds/moment-timezone-with-data.min.js", "htdocs/js/external/" ],
|
||||
[ "symlinkCompress", "node_modules/jstimezonedetect/dist/jstz.min.js", "htdocs/js/external/" ],
|
||||
|
||||
[ "symlinkFile", "node_modules/pixl-webapp/js", "htdocs/js/common" ],
|
||||
[ "symlinkFile", "node_modules/pixl-webapp/css/base.css", "htdocs/css/" ],
|
||||
[ "copyFiles", "node_modules/pixl-webapp/fonts/*", "htdocs/fonts/" ],
|
||||
|
||||
[ "chmodFiles", "755", "bin/*" ]
|
||||
],
|
||||
"dev": [
|
||||
[ "deleteFiles", "htdocs/css/_combo*" ],
|
||||
[ "deleteFiles", "htdocs/js/_combo*" ],
|
||||
[ "deleteFile", "htdocs/index.html" ],
|
||||
[ "deleteFile", "htdocs/index.html.gz" ],
|
||||
[ "symlinkFile", "htdocs/index-dev.html", "htdocs/index.html" ],
|
||||
[ "symlinkFile", "sample_conf", "conf" ]
|
||||
],
|
||||
"dist": [
|
||||
{
|
||||
"action": "generateSecretKey",
|
||||
"file": "sample_conf/config.json",
|
||||
"key": "secret_key"
|
||||
},
|
||||
[ "copyDir", "sample_conf", "conf", true ],
|
||||
[ "copyFile", "htdocs/index-dev.html", "htdocs/index.html" ],
|
||||
{
|
||||
"action": "bundleCompress",
|
||||
"uglify": false,
|
||||
"header": "/* Copyright (c) PixlCore.com, MIT License. https://github.com/jhuckaby/Cronicle */",
|
||||
"dest_bundle": "htdocs/js/_combo.js",
|
||||
"html_file": "htdocs/index.html",
|
||||
"match_key": "COMBINE_SCRIPT",
|
||||
"dest_bundle_tag": "<script src=\"js/_combo.js\"></script>"
|
||||
},
|
||||
{
|
||||
"action": "bundleCompress",
|
||||
"strip_source_maps": true,
|
||||
"dest_bundle": "htdocs/css/_combo.css",
|
||||
"html_file": "htdocs/index.html",
|
||||
"match_key": "COMBINE_STYLE",
|
||||
"dest_bundle_tag": "<link rel=\"stylesheet\" href=\"css/_combo.css\">"
|
||||
},
|
||||
{
|
||||
"action": "printMessage",
|
||||
"lines": [
|
||||
"Welcome to Cronicle!",
|
||||
"First time installing? You should configure your settings in '/opt/cronicle/conf/config.json'.",
|
||||
"Next, if this is a master server, type: '/opt/cronicle/bin/control.sh setup' to init storage.",
|
||||
"Then, to start the service, type: '/opt/cronicle/bin/control.sh start'.",
|
||||
"For full docs, please visit: http://github.com/jhuckaby/Cronicle",
|
||||
"Enjoy!"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
20
workspaces/base-workspace/Cronicle-0.8.61/sample_conf/ssl.crt
Executable file
|
@ -0,0 +1,20 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDMjCCAhqgAwIBAgIJANQO8EFYRaZiMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV
|
||||
BAMTD3d3dy5leGFtcGxlLmNvbTAeFw0xMzA2MjUxNDMxMDFaFw0yMzA2MjMxNDMx
|
||||
MDFaMBoxGDAWBgNVBAMTD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB
|
||||
BQADggEPADCCAQoCggEBANhUOPoivca6J5zWqqp3xkd/iTjE3ME7/36wTprNDIt4
|
||||
VQpiQfMMUeE4UQadivNJJnzAsB/LtZs2T3h//InwhC+40bsmtj0DVqGadthr8x8l
|
||||
tV15vfwztMQ5e1eU3u0oBQzS3o1sLplD8U88GeTAqamgQLG769kHbqqsxA/lYuAh
|
||||
O3SRHxkpEgW5pC72lbsTqE8U6ipLry3S3wT1+4BCFC/gFtdg4ILjgqfPNAvzR72R
|
||||
X+kdy2jJ5fAaE3C/ca8uRkN+rqt2QV5c1MP12UNn5OEQntMNrB6/91V3jfN3UEEF
|
||||
s9hLZSstPIxYye2B7PkFlW3irR34HuDrSki0u0k4xIkCAwEAAaN7MHkwHQYDVR0O
|
||||
BBYEFHb75UVd2UnNO2ml+y227nRujASpMEoGA1UdIwRDMEGAFHb75UVd2UnNO2ml
|
||||
+y227nRujASpoR6kHDAaMRgwFgYDVQQDEw93d3cuZXhhbXBsZS5jb22CCQDUDvBB
|
||||
WEWmYjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQDXiSZaNq0p0IQq
|
||||
tttyaksuIITs7WYusXhZiMufO9rbIiQErCgTZuUE/DahIPQFKAUwTS6VQO9rkFTj
|
||||
1nfDJe/SD9EoIco7gxe/9a/FiGi53uTlVq8F87clr87NCh+t8VMwLMO/43tsgl2U
|
||||
A0kaExBo1roJwJcrADNsyCfSsB8n2y2n7Q8QOdJ0HOzHT0vvYUJZaOprV9dp+8Yj
|
||||
+raVZRibPA+H4jUEBHk387knpUx5tWeJd+7RCA1pCcAU2b3lfcL6zbiUGhJdrAbx
|
||||
LZ3egEKpKR4Ld7w1NN7rQaGQ3FWlIdIGUGFiMsT5LdubC6ABnqdxd+Sg6sfnsbcK
|
||||
/x+0/eC8
|
||||
-----END CERTIFICATE-----
|
27
workspaces/base-workspace/Cronicle-0.8.61/sample_conf/ssl.key
Executable file
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEA2FQ4+iK9xronnNaqqnfGR3+JOMTcwTv/frBOms0Mi3hVCmJB
|
||||
8wxR4ThRBp2K80kmfMCwH8u1mzZPeH/8ifCEL7jRuya2PQNWoZp22GvzHyW1XXm9
|
||||
/DO0xDl7V5Te7SgFDNLejWwumUPxTzwZ5MCpqaBAsbvr2QduqqzED+Vi4CE7dJEf
|
||||
GSkSBbmkLvaVuxOoTxTqKkuvLdLfBPX7gEIUL+AW12DgguOCp880C/NHvZFf6R3L
|
||||
aMnl8BoTcL9xry5GQ36uq3ZBXlzUw/XZQ2fk4RCe0w2sHr/3VXeN83dQQQWz2Etl
|
||||
Ky08jFjJ7YHs+QWVbeKtHfge4OtKSLS7STjEiQIDAQABAoIBADv3hN/Z9496FPcG
|
||||
DsM4do9lTC2fbK5oKlf9GZ0R0DNtRO2e9TchqCTtjpBt5ZGxKmkUpP37YzlGYds+
|
||||
Z0v5jzsHWaQug///x+j+P4mYywlMU604zTB3SNnIMWfCzdUh7dxzK9w6K+Syj9bu
|
||||
CyN9QMrTsHtUY3mC9Ot8/tCFPtZv/SQY2Y9kqCb2RJJ/vTcTcsjEXjMHFuCv9+KZ
|
||||
VUGX1sm+VtYY5nUtNEmVYWxCfrU5YRxL6W8iuwdWveGIrvkAnktdmnFPM+MGztmh
|
||||
7MCxE5X8m8+yUDo/SqUeqniX6nYq6/I7MpNG3XVEE1OEY5o/20+cDxXci9Pn6AXj
|
||||
TuiZ/XECgYEA+wxFtLh4nna1UE3+nPHel13IC90cneqdRT8wISNXF+Sm1rRfSI4s
|
||||
r461xmA/BwEFhWrO1xbNi15978zsTUUaPWka4sqh7ZI+/m93C3vbvI1xWGttSqg0
|
||||
pWI/RjO3rM4vnd0tMgmmNCc/47IB/DXiKJPOcfUScyCS+4j+1ghJ250CgYEA3Jig
|
||||
5jltZXyUTpHP+FoLV+BGjkxu1S35xjWHg6i6j9LPYxsMWgXTc7OhmCXEBY5lPPSF
|
||||
hTjqKMvzr79obzPKHt083USzZIyDC0P7uCsmBL6oiTVN1XhpO/1gG8tFHLuZa6B3
|
||||
Jd074vrsrAMcEottoxyST9jgdiB7Fh47Rjfqht0CgYANBTLsT5D57wAyXQkyjJzV
|
||||
zuhcLSiZzBxCBifx4ApZU+OPSSWT9sO8izNESaObMmNd6w81OpqIeusfL8qlq0rU
|
||||
Goppbsb9MlOQEKnk75SS7+cMBe5SK+0nErRjaLVDAiKYFmuMp9F17P80SPwvX4AO
|
||||
SLQxVtuRGwRkhVNqOF3URQKBgAdyD1w16/9U6RyNx1s2jtN0em0rH0KKvrd17xD+
|
||||
jO11zBIoQ452S+DH21hrTeZyG/CmwCry9NRTrfHsn/XA5b2M8hT10KhAJdwne0OI
|
||||
EUxvsviOmAXwfnzL3IaToc2Kd28uh1b71J2gooRbxoLJufWbbUTMqSbTidQBSTbh
|
||||
hETxAoGAN4E5AJF+CiSzG0TO4BbKpfnmr6HV/mD4zMiUHYNPQDHRoFo0hxsPEwzj
|
||||
CY2RL6tBnCdzFtZiZYsX7D0uAma0dVRyVvlkDVIH2A65T4OUcXbeaB6Jcf2Z706f
|
||||
iNPBZa/RKsDJ/RTeZDP8NZfhfhqJq2Nvp/1/hMGCbWfHshltL0M=
|
||||
-----END RSA PRIVATE KEY-----
|
54
workspaces/base-workspace/Dockerfile
Executable file
|
@ -0,0 +1,54 @@
|
|||
ARG docker_registry=docker.io/alnoda
|
||||
ARG image_tag=18.04-0.5
|
||||
|
||||
FROM ${docker_registry}/ubuntu-workspace:${image_tag}
|
||||
|
||||
USER root
|
||||
|
||||
################################################################# TOOLS: cronicle, filebrowser, ungit, static server
|
||||
|
||||
|
||||
COPY Cronicle-0.8.61 /opt/cronicle
|
||||
|
||||
RUN echo "------------------------------------------------------ cronicle" \
|
||||
&& mkdir -p /opt/cronicle \
|
||||
&& cd /opt/cronicle && nodeenv --node=12.18.3 --npm=6.0.0 env \
|
||||
&& cd /opt/cronicle && . env/bin/activate && npm install; node bin/build.js dist
|
||||
|
||||
COPY cronicle-config.json /opt/cronicle/conf/config.json
|
||||
COPY supervisord-workspace-base.conf /etc/supervisord/
|
||||
COPY filebrowser.json /opt/filebrowser/.filebrowser.json
|
||||
|
||||
COPY mkdocs /home/docs
|
||||
COPY README.md /home/docs/docs/get-started.md
|
||||
COPY mkdocs-requirements.txt /home/abc/installed-python-packages/mkdocs-requirements.txt
|
||||
|
||||
RUN echo "------------------------------------------------------ filebrowser, ungit, static server" \
|
||||
&& apt-get -y update \
|
||||
&& apt-get install -y apache2-utils \
|
||||
&& mkdir -p -m 777 /opt/filebrowser \
|
||||
&& curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash \
|
||||
&& apt-get install -y ssh net-tools --no-install-recommends \
|
||||
&& mkdir -p /opt/ungit \
|
||||
&& cd /opt/ungit && nodeenv --node=12.18.3 --npm=6.0.0 env \
|
||||
&& cd /opt/ungit && . env/bin/activate && npm install -g ungit@1.5.9 \
|
||||
&& mkdir -p /opt/serve \
|
||||
&& cd /opt/serve && nodeenv --node=12.18.3 --npm=6.0.0 env \
|
||||
&& cd /opt/serve && . env/bin/activate && npm install -g serve \
|
||||
&& echo "------------------------------------------------------ mkdocs" \
|
||||
&& pip install -r /home/abc/installed-python-packages/mkdocs-requirements.txt \
|
||||
&& echo "------------------------------------------------------ user" \
|
||||
&& mkdir -p /home/static-server \
|
||||
&& chown -R abc /opt/cronicle \
|
||||
&& chown -R abc /opt/filebrowser \
|
||||
&& chown -R abc /home/static-server \
|
||||
&& chown -R abc /home/docs \
|
||||
&& chown -R abc /opt/ungit \
|
||||
&& chown -R abc /opt/serve \
|
||||
&& mkdir -p /var/log/cronicle && chown -R abc /var/log/cronicle \
|
||||
&& mkdir -p /var/log/filebrowser && chown -R abc /var/log/filebrowser \
|
||||
&& mkdir -p /var/log/ungit && chown -R abc /var/log/ungit \
|
||||
&& mkdir -p /var/log/static-file-server && chown -R abc /var/log/static-file-server \
|
||||
&& mkdir -p /var/log/mkdocs && chown -R abc /var/log/mkdocs
|
||||
|
||||
USER abc
|
387
workspaces/base-workspace/README.md
Normal file
|
@ -0,0 +1,387 @@
|
|||
|
||||
|
||||
# Base-workspace
|
||||
|
||||
Base-Workspace is an enhanced docker image with Ubuntu and additional tools set up in order to develop applications directly inside docker container,
|
||||
and have your dependencies, configuration and credential files, ssh keys and data isolated from other environments.
|
||||
|
||||
## Contents
|
||||
|
||||
* [Use-cases](#use-cases)
|
||||
* [Features](#features)
|
||||
* [Launch Workspace](#launch-workspace)
|
||||
* [Workspace terminal](#workspace-terminal)
|
||||
* [Multiple workspaces](#multipl-workspaces)
|
||||
* [Open more ports](#open-more-ports)
|
||||
* [Run as root](#run-as-root)
|
||||
* [Docker in docker](#docker-in-docker)
|
||||
* [Run on remote server](#run-on-remote-server)
|
||||
* [Use Workspace](#use-workspace)
|
||||
* [Install new packages](#install-new-packages)
|
||||
* [Schedule jobs with Cron](#schedule-jobs-with-cron)
|
||||
* [Python](#python)
|
||||
* [Node.js](#node.js)
|
||||
* [Run applications and services inside the workspace](#run-applications-and-service-inside-the-workspace)
|
||||
* [Manage workspaces](#manage-workspaces)
|
||||
* [Start and stop containers](#start-and-stop-containers)
|
||||
* [Create new workspace image](#create-new-workspace-image)
|
||||
* [Manage workspace images](#manage-workspace-images)
|
||||
* [Save and load workspace images](#save-and-load-workspace-images)
|
||||
|
||||
|
||||
## Use-cases
|
||||
|
||||
Base-Workspace was created as an intermediary step between `ubuntu-workspace` that has only terminal-based tools
|
||||
and `workspace-in-docker` that includes a set of WEB-UI tools which trannsform docker into the full-power development
|
||||
environment.
|
||||
|
||||
Base-workspace does not include IDE, and serves as a building base for other workspaces with different IDEs.
|
||||
|
||||
## Features
|
||||
|
||||
Being an extension of [ubuntu-workspace-in-docker](https://github.com/Alnoda/ubuntu-workspace-in-docker) this image has all the features that
|
||||
ubuntu-workspace has.
|
||||
|
||||
Workspace includes several open-source tools with Web GUI:
|
||||
|
||||
- [**FileBrowser**](./features.md#filebrowser) - manage files and folders inside the workspace, and exchange data between local environment and the workspace
|
||||
- [**Cronicle**](./features.md#cronicle) - task scheduler and runner, with a web based front-end UI. It handles both scheduled, repeating and on-demand jobs, targeting any number of worker servers, with real-time stats and live log viewer.
|
||||
- [**Static File Server**](./features.md#static-file-server) - view any static html sites as easy as if you do it on your local machine. Serve static websites easily.
|
||||
- [**Ungit**](./features.md#ungit) - rings user friendliness to git without sacrificing the versatility of it.
|
||||
- [**MkDocs**](./docs.md) - create documentation for your workspace or project with only markdown.
|
||||
|
||||
Despite having WEB UI tools, Base-Workspace does not include IDE. This workspace serves eitehr of 2 use-cases:
|
||||
|
||||
- for those who prefer coding in terminal-based editors (emacs, vim, nano etc.)
|
||||
- for customization and adding IDE of chioce or closed-source ide
|
||||
|
||||
## Launch Workspace
|
||||
|
||||
In order to avoid confusion, the following convention is adopted:
|
||||
|
||||
```sh
|
||||
command to execute outside of the workspace
|
||||
```
|
||||
|
||||
> `command to execute inside the workspace (after entering running docker container)`
|
||||
|
||||
To start Base Workspace simply execute in terminal
|
||||
|
||||
```sh
|
||||
docker run --name space-1 -d -p 8020-8030:8020-8030 alnoda/base-workspace
|
||||
```
|
||||
|
||||
### Workspace terminal
|
||||
|
||||
enter into the running workspace container
|
||||
```sh
|
||||
docker exec -it space-1 /bin/zsh
|
||||
```
|
||||
|
||||
If you don't want to use z-shell
|
||||
```
|
||||
docker exec -it space-1 /bin/bash
|
||||
```
|
||||
|
||||
You can work in Ubuntu terminal now. Execute the followinng command to know your workspace user
|
||||
|
||||
> `whoami`
|
||||
|
||||
### Multiple workspaces
|
||||
|
||||
Every workspace requires range of ports. If one workspace is up and running, the ports 8020-8030 are taken.
|
||||
|
||||
In order to start another workspace it is necessary either to stop currently runnning workspace, or to run another workspace
|
||||
on the different port range.
|
||||
|
||||
If you are planning to run multiple workspaces at the same time, you can run second workspace with different port range
|
||||
|
||||
```sh
|
||||
docker run --name space-2 -d -p 8040-8050:8020-8030 -e ENTRY_PORT=8040 alnoda/base-workspace
|
||||
```
|
||||
|
||||
Notice that in addition we need to set environmental variable ENTRY_PORT, which should be equal to the first port in the new range.
|
||||
This is needed for the documentation main page to set up correct links to other tools (Filebrowser, Cronicle etc.)
|
||||
|
||||
|
||||
### Open more ports
|
||||
We started workspace container with a port range mapped "-p 8020-8030". If you are planning to expose more applications
|
||||
from inside of a container, add additional port mapping, for example
|
||||
|
||||
```sh
|
||||
docker run --name space-1 -d -p 8020-8030:8020-8030 -p 8080:8080 alnoda/base-workspace
|
||||
```
|
||||
You can add multiple port mappings:
|
||||
```sh
|
||||
docker run --name space-1 -d -p 8020-8030:8020-8030 -p 8080:8080 -p 443:443 alnoda/base-workspace
|
||||
```
|
||||
|
||||
**NOTE:** It is not a problem if you don't expose any ports, but later on realise you need them -
|
||||
you will just create new image, and run it exposing the required port (look in the section [Create new image](#create-new-image))
|
||||
|
||||
### Run as root
|
||||
The default user is **abc** with passwordless sudo to install packages. If you'd rather work as root, then you should ssh into running container as
|
||||
```sh
|
||||
docker exec -it --user=root space-1 /bin/zsh
|
||||
```
|
||||
You can of course open several terminals to the same running containner as both abc and root users at the same time.
|
||||
|
||||
### Docker in docker
|
||||
|
||||
It is possible to work with docker directly from the workspace.
|
||||
|
||||
```
|
||||
docker run --name space-1 -d -p 8020-8030:8020-8030 -v /var/run/docker.sock:/var/run/docker.sock alnoda/base-workspace
|
||||
```
|
||||
|
||||
NOTE: in order to use docker in docker you need to or enter into the workspace container as root
|
||||
```sh
|
||||
docker exec -it --user=root space-1 /bin/zsh
|
||||
```
|
||||
|
||||
### Run on remote server
|
||||
|
||||
Because workspace is just a docker image, running it in cloud is as easy as running it on local laptop. There are only 3 steps:
|
||||
|
||||
- get virtual server on your favourite cloud (Digital Ocean, Linode, AWS, GC, Azure ...)
|
||||
- [install docker](https://docs.docker.com/engine/install/) on this server
|
||||
- ssh to the remote server and start workspace. Add envronmental variable `-e WRK_HOST="<ip-of-your-remote-server>"`
|
||||
|
||||
```
|
||||
docker run --name space-1 -d -p 8020-8030:8020-8030 -e WRK_HOST="<ip-of-your-remote-server>" alnoda/base-workspace
|
||||
```
|
||||
|
||||
if docker-in-docker needed then
|
||||
|
||||
```
|
||||
docker run --name space-1 -d -p 8020-8030:8020-8030 -e WRK_HOST="<ip-of-your-remote-server>" -v /var/run/docker.sock:/var/run/docker.sock alnoda/base-workspace
|
||||
```
|
||||
|
||||
Open in your browser `<ip-of-your-remote-server>:8020`
|
||||
|
||||
|
||||
## Use Workspace
|
||||
|
||||
The common actions inside the workspace include
|
||||
|
||||
- installation of new applications and runtimes
|
||||
- edit files, write code, scripts
|
||||
- build, compile and execute code
|
||||
- start/stop applications and services
|
||||
- schedule tasks and scripts
|
||||
- process data
|
||||
|
||||
### Install new packages
|
||||
Install new packages with ```sudo apt install```. The default abc user is allowed to install packages.
|
||||
|
||||
For example, in order to install [Emacs text editor](https://www.gnu.org/software/emacs/) make sure you
|
||||
have entered running docker container (of the workspace), and execute in terminal
|
||||
|
||||
> `sudo apt install emacs`
|
||||
|
||||
|
||||
### Schedule jobs with Cron
|
||||
|
||||
Schedule execution of any task with cron - a time-based job scheduler in Unix-like computer operating systems.
|
||||
|
||||
In order to create scheduled job enter running docker container, and execute in terminal
|
||||
|
||||
> `crontab -e`
|
||||
|
||||
*(chose [1] nano as editor on the first time)*
|
||||
In the end of the opened file add line
|
||||
|
||||
> `* * * * * echo $(whoami) >> /home/cron.txt`
|
||||
|
||||
|
||||
This will print every minute username to file */home/cron.txt* . *(Hit Ctrl+X to exit nano)*
|
||||
|
||||
Hint: example of cron job definition:
|
||||
```
|
||||
.---------------- minute (0 - 59)
|
||||
| .------------- hour (0 - 23)
|
||||
| | .---------- day of month (1 - 31)
|
||||
| | | .------- month (1 - 12) OR jan,feb,mar,apr ...
|
||||
| | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
|
||||
| | | | |
|
||||
* * * * * command to be executed
|
||||
```
|
||||
|
||||
**NOTE** you can disconnect from the image and close terminal - cron will continue working.
|
||||
|
||||
> In addition to the commonly known ***cron scheduler*** you can use Cronicle - the tool with Web UI and great features
|
||||
> which is bundeled together with the Base-Workspace.
|
||||
|
||||
### Python
|
||||
Python and Pip are installed. To start python console, enter running docker container, and execute in terminal
|
||||
|
||||
> `python`
|
||||
|
||||
install python package with pip, for
|
||||
|
||||
> `pip install pandas`
|
||||
|
||||
If you are planning to work with python, we recommend to install IPython, that provides a rich toolkit to help
|
||||
you make the most of using Python interactively. Install and start ipython
|
||||
|
||||
> ```pip install ipython```
|
||||
> `ipython`
|
||||
|
||||
### Node.js
|
||||
We recommend to use nodeenv to create different node environments.
|
||||
|
||||
For example, create folder npmgui, and activate environment with node v. 12.18.3 and npm v.6.0.0 (make sure you are inside workspace docker container)
|
||||
|
||||
> `cd /home`
|
||||
> `mkdir npmgui; cd npmgui`
|
||||
> `nodeenv --node=12.18.3 --npm=6.0.0 env`
|
||||
|
||||
Let's install package and start node application
|
||||
|
||||
> `. env/bin/activate && npm i -g npm-gui`
|
||||
> `npm-gui 0.0.0.0:8030`
|
||||
|
||||
Open your browser on http://localhost:8030/
|
||||
|
||||
**NOTE:** If you close terminal, the application will stop. See how to [start applications that reamin live after closing a workspace terminal](#run-applications-and-services-inside-the-workspace)
|
||||
|
||||
|
||||
### Run applications and services inside the workspace
|
||||
|
||||
If you want application to keep running after terminal is closed start it with **"&!"** at the end.
|
||||
|
||||
For example, enter into the running workspace container, and start the example node application from the previous section:
|
||||
|
||||
> `cd /home/npmgui`
|
||||
> `. env/bin/activate && npm i -g npm-gui &!`
|
||||
|
||||
Now, if you disconnect from the workspace and close terminal, the application will still continue running in the workspace, untill [workspace is stopped](#start-and-stop-workspaces).
|
||||
|
||||
If you want application to start automatically each time workspaces is restarted, or the new workspace is created, see [running applications permanently](extend.md#add-applications-and-services)
|
||||
|
||||
|
||||
## Manage workspaces
|
||||
|
||||
Workspace is just a docker container. You can start, stop, delete and do anything you can do with docker images and containers.
|
||||
|
||||
There are two concepts to keep in mind: **images** and **containers**. Images are workspace blueprints. For example, **alnoda/base-workspace** -
|
||||
is an image. When you execute this command
|
||||
|
||||
```sh
|
||||
docker run --name space-1 -d -p 8020-8030:8020-8030 alnoda/base-workspace
|
||||
```
|
||||
you create container called **space-1** from the image **alnoda/base-workspace**. You can create any number of containers, but you need to
|
||||
[map different ports to each of them](#multiple-workspaces).
|
||||
|
||||
Container - is your workspace. You can start, stop and delete them. You can run multiple workspace containers at the same time, or work with
|
||||
one workspace at a time.
|
||||
|
||||
From the workspace (which is a container) you can create new image. This is called **commit docker image**.
|
||||
Essentially, this means *"take my workspace and create new image with all the changes I've done in my workspace*"
|
||||
|
||||
### Start and stop workspaces
|
||||
|
||||
The workspace started in daemon mode will continue working in the background.
|
||||
|
||||
See all the running docker containers
|
||||
|
||||
```
|
||||
docker ps
|
||||
```
|
||||
|
||||
Stop workspace
|
||||
|
||||
```sh
|
||||
docker stop space-1
|
||||
```
|
||||
Workspace is stopped. All the processes and cron jobs are not running.
|
||||
|
||||
See all docker conntainers, including stopped
|
||||
|
||||
```
|
||||
docker ps -a
|
||||
```
|
||||
|
||||
Start workspace again. Processes and cron jobs are resumed.
|
||||
|
||||
```sh
|
||||
docker start space-1
|
||||
```
|
||||
|
||||
Delete workspace container (all work will be lost)
|
||||
|
||||
```
|
||||
docker rm space-1
|
||||
```
|
||||
|
||||
### Create new workspace image
|
||||
|
||||
Having made changes, you can commit them creating new image of the workspace. In order to create new workspace image with the
|
||||
name "space-image" and version "0.2" execute
|
||||
|
||||
```
|
||||
docker commit space-1 space-image:0.2
|
||||
```
|
||||
|
||||
Run new workspace with
|
||||
|
||||
```
|
||||
docker run --name space2 -d space-image:0.2
|
||||
```
|
||||
|
||||
The new workspace accommodates all the changes that you've made in your space-1. Hence you can have versions of your workspaces.
|
||||
Create different versions before the important changes.
|
||||
|
||||
### Manage workspace images
|
||||
|
||||
See all docker images
|
||||
|
||||
```
|
||||
docker images
|
||||
```
|
||||
|
||||
Delete workspace image entirely
|
||||
|
||||
```
|
||||
docker rmi -f alnoda/base-workspace
|
||||
```
|
||||
|
||||
**NOTE:** you cannot delete image if there is a running container created from it. Stop container first.
|
||||
|
||||
### Save and load workspace images
|
||||
|
||||
After you commit workspace container, and create new image out of it, you can push it to your docker registry or save it as a file.
|
||||
|
||||
**SAVING IMAGE AS FILE**
|
||||
|
||||
Assuming you created new image **space-image:0.4** from your workspace, you can save it as a tar file
|
||||
|
||||
```
|
||||
docker save space-image:0.4 > space-image-0.4.tar
|
||||
```
|
||||
|
||||
We can delete the image with
|
||||
|
||||
```
|
||||
docker rmi -f space-image:0.4
|
||||
```
|
||||
|
||||
And restore it from the tar file
|
||||
|
||||
```
|
||||
docker load < space-image-0.4.tar
|
||||
```
|
||||
|
||||
**PUSHING IMAGE TO YOUR REGISTRY**
|
||||
|
||||
A better way to manage images is docker registries. You can use docker registries in multiple clouds. They are cheap annd very convenient.
|
||||
Check out for example, [Registry in DigitalOcean](https://www.digitalocean.com/products/container-registry/) or in [Scaleway container registry](https://www.scaleway.com/en/container-registry/). There are more.
|
||||
|
||||
Pushing image to registry is merely 2 extra commands: 1) tag image; 2) push image
|
||||
|
||||
You will be able to pull image on any device, local or cloud.
|
||||
|
||||
|
||||
|
||||
|
||||
|
137
workspaces/base-workspace/cronicle-config.json
Executable file
|
@ -0,0 +1,137 @@
|
|||
{
|
||||
"base_app_url": "http://localhost:3012",
|
||||
"email_from": "jobs@cronicle.com",
|
||||
"smtp_hostname": "localhost",
|
||||
"smtp_port": 25,
|
||||
"secret_key": "68c55b2c6c578b22df5872e002c9b679",
|
||||
|
||||
"log_dir": "logs",
|
||||
"log_filename": "[component].log",
|
||||
"log_columns": ["hires_epoch", "date", "hostname", "pid", "component", "category", "code", "msg", "data"],
|
||||
"log_archive_path": "logs/archives/[yyyy]/[mm]/[dd]/[filename]-[yyyy]-[mm]-[dd].log.gz",
|
||||
"log_crashes": true,
|
||||
"copy_job_logs_to": "",
|
||||
"queue_dir": "queue",
|
||||
"pid_file": "logs/cronicled.pid",
|
||||
"debug_level": 9,
|
||||
"maintenance": "04:00",
|
||||
"list_row_max": 10000,
|
||||
"job_data_expire_days": 180,
|
||||
"child_kill_timeout": 10,
|
||||
"dead_job_timeout": 120,
|
||||
"master_ping_freq": 20,
|
||||
"master_ping_timeout": 60,
|
||||
"udp_broadcast_port": 3014,
|
||||
"scheduler_startup_grace": 10,
|
||||
"universal_web_hook": "",
|
||||
"track_manual_jobs": false,
|
||||
"max_jobs": 0,
|
||||
|
||||
"server_comm_use_hostnames": false,
|
||||
"web_direct_connect": false,
|
||||
"web_socket_use_hostnames": false,
|
||||
|
||||
"job_memory_max": 1073741824,
|
||||
"job_memory_sustain": 0,
|
||||
"job_cpu_max": 0,
|
||||
"job_cpu_sustain": 0,
|
||||
"job_log_max_size": 0,
|
||||
"job_env": {},
|
||||
|
||||
"web_hook_text_templates": {
|
||||
"job_start": "Job started on [hostname]: [event_title] [job_details_url]",
|
||||
"job_complete": "Job completed successfully on [hostname]: [event_title] [job_details_url]",
|
||||
"job_failure": "Job failed on [hostname]: [event_title]: Error [code]: [description] [job_details_url]",
|
||||
"job_launch_failure": "Failed to launch scheduled event: [event_title]: [description] [edit_event_url]"
|
||||
},
|
||||
|
||||
"client": {
|
||||
"name": "Cronicle",
|
||||
"debug": 0,
|
||||
"privilege_list": [
|
||||
{ "id": "admin", "title": "Administrator" },
|
||||
{ "id": "create_events", "title": "Create Events" },
|
||||
{ "id": "edit_events", "title": "Edit Events" },
|
||||
{ "id": "delete_events", "title": "Delete Events" },
|
||||
{ "id": "run_events", "title": "Run Events" },
|
||||
{ "id": "abort_events", "title": "Abort Events" },
|
||||
{ "id": "state_update", "title": "Toggle Scheduler" }
|
||||
],
|
||||
"new_event_template": {
|
||||
"enabled": 1,
|
||||
"params": {},
|
||||
"timing": { "minutes": [0] },
|
||||
"max_children": 1,
|
||||
"timeout": 3600,
|
||||
"catch_up": 0,
|
||||
"queue_max": 1000
|
||||
}
|
||||
},
|
||||
|
||||
"Storage": {
|
||||
"engine": "Filesystem",
|
||||
"list_page_size": 50,
|
||||
"concurrency": 4,
|
||||
"log_event_types": { "get": 1, "put": 1, "head": 1, "delete": 1, "expire_set": 1 },
|
||||
|
||||
"Filesystem": {
|
||||
"base_dir": "data",
|
||||
"key_namespaces": 1
|
||||
}
|
||||
},
|
||||
|
||||
"WebServer": {
|
||||
"http_port": 8023,
|
||||
"http_htdocs_dir": "htdocs",
|
||||
"http_max_upload_size": 104857600,
|
||||
"http_static_ttl": 3600,
|
||||
"http_static_index": "index.html",
|
||||
"http_server_signature": "Cronicle 1.0",
|
||||
"http_gzip_text": true,
|
||||
"http_timeout": 30,
|
||||
"http_regex_json": "(text|javascript|js|json)",
|
||||
"http_response_headers": {
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
},
|
||||
|
||||
"https": false,
|
||||
"https_port": 3013,
|
||||
"https_cert_file": "conf/ssl.crt",
|
||||
"https_key_file": "conf/ssl.key",
|
||||
"https_force": false,
|
||||
"https_timeout": 30,
|
||||
"https_header_detect": {
|
||||
"Front-End-Https": "^on$",
|
||||
"X-Url-Scheme": "^https$",
|
||||
"X-Forwarded-Protocol": "^https$",
|
||||
"X-Forwarded-Proto": "^https$",
|
||||
"X-Forwarded-Ssl": "^on$"
|
||||
}
|
||||
},
|
||||
|
||||
"User": {
|
||||
"session_expire_days": 30,
|
||||
"max_failed_logins_per_hour": 5,
|
||||
"max_forgot_passwords_per_hour": 3,
|
||||
"sort_global_users": true,
|
||||
"use_bcrypt": true,
|
||||
|
||||
"email_templates": {
|
||||
"welcome_new_user": "conf/emails/welcome_new_user.txt",
|
||||
"changed_password": "conf/emails/changed_password.txt",
|
||||
"recover_password": "conf/emails/recover_password.txt"
|
||||
},
|
||||
|
||||
"free_accounts": 1,
|
||||
"default_privileges": {
|
||||
"admin": 1,
|
||||
"create_events": 1,
|
||||
"edit_events": 1,
|
||||
"delete_events": 1,
|
||||
"run_events": 1,
|
||||
"abort_events": 1,
|
||||
"state_update": 1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
16
workspaces/base-workspace/filebrowser.json
Executable file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"port": 8021,
|
||||
"baseURL": "",
|
||||
"address": "0.0.0.0",
|
||||
"log": "stdout",
|
||||
"database": "/opt/filebrowser/database.db",
|
||||
"root": "/home",
|
||||
"allowEdit": true,
|
||||
"allowNew": true,
|
||||
"allowCommands": true,
|
||||
"commands": ["apt", "ls", "cd", "rm", "cp", "mv", "cat", "echo", "tar", "zip", "unzip"],
|
||||
"noAuth": true,
|
||||
"auth": {
|
||||
"method": "noauth"
|
||||
}
|
||||
}
|
6
workspaces/base-workspace/mkdocs-requirements.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
mkdocs==1.2.1
|
||||
# https://squidfunk.github.io/mkdocs-material/getting-started/#installation
|
||||
mkdocs-material==7.1.8
|
||||
# https://github.com/fralau/mkdocs_macros_plugin
|
||||
mkdocs-macros-plugin==0.5.12
|
||||
|
68
workspaces/base-workspace/mkdocs/.gitignore
vendored
Normal file
|
@ -0,0 +1,68 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# IPython Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# virtualenv
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# MkDocs documentation
|
||||
site/
|
25
workspaces/base-workspace/mkdocs/docs/README.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
**This is a starting point to create docs for this workspace!**
|
||||
|
||||
In order to change this page, simply modify the file `/home/docs/docs/README.md`. Changes will be applied automatically.
|
||||
|
||||
In order to add a new doc file, it is enough to create a file in the folder `/home/docs/docs` and add respective entry
|
||||
to the configuratiion file `/home/docs/mkdcs.yaml`.
|
||||
|
||||
For example, [enter the terminal in the workspace](get-started.md#workspace-terminal),
|
||||
and create new documentation file with some text at your will, and save changes
|
||||
|
||||
> `nano /home/docs/docs/new.md`
|
||||
|
||||
edit file `mkdcs.yaml`
|
||||
|
||||
> `nano /home/docs/mkdcs.yaml`
|
||||
|
||||
Add record about the new file to **nav**, and save changes
|
||||
|
||||
```yaml
|
||||
nav:
|
||||
- Home: pages/home/home.md
|
||||
- About: README.md
|
||||
- Get started: get-started.md
|
||||
- New: new.md
|
||||
```
|
70
workspaces/base-workspace/mkdocs/docs/assets/Alnoda-logo.svg
Normal file
|
@ -0,0 +1,70 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1280" height="1024" viewBox="0 0 10000 9600" xml:space="preserve">
|
||||
<desc>Created with Fabric.js 3.6.3</desc>
|
||||
<defs>
|
||||
</defs>
|
||||
<g transform="matrix(2,0,0,2,640,512)" id="background-logo" >
|
||||
</g>
|
||||
<g transform="matrix(2,0,0,2,640,416.3)" id="logo-logo" >
|
||||
<g transform="matrix(18.9,0,0,24.4,-502.2,-1009.3)" style="" paint-order="stroke" >
|
||||
<g transform="matrix(0.2,0,0,-0.2,0,-61.6)" >
|
||||
<path style="fill: rgb(64, 50, 44); fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-dasharray: none; stroke-dashoffset: 0; stroke-miterlimit: 4; opacity: 1" paint-order="stroke" transform="translate(-1625,-1745)" d="M 3062 1050.1 c -466.3 107.3 -950.8 107.3 -1417.1 0 c -16.2 -3.7 -26.6 -18.2 -23.3 -32.1 c 3.4 -13.9 19 -22.1 34.9 -18.4 c 458.6 105.5 935.2 105.5 1393.9 0 c 15.9 -3.7 31.5 4.5 34.9 18.4 c 3.4 13.9 -7 28.4 -23.3 32.1" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(0.2,0,0,-0.2,-95.7,12.6)" >
|
||||
<path style="fill: rgb(64, 50, 44); fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-dasharray: none; stroke-dashoffset: 0; stroke-miterlimit: 4; opacity: 1" paint-order="stroke" transform="translate(-1210,-1423.3)" d="M 1847.7 878.7 c 0 52.1 44.1 94.1 97 90.6 c 48.2 -3.2 84.6 -45.5 84.6 -93.8 V 490.5 c 0 -14.3 11.6 -25.9 25.9 -25.9 c 14.3 0 25.9 11.6 25.9 25.9 v 384 c 0 76.7 -58.8 142.8 -135.5 146.6 c -81.9 4 -149.8 -61.4 -149.8 -142.5 V 490.5 c 0 -14.3 11.6 -25.9 25.9 -25.9 c 14.3 0 25.9 11.6 25.9 25.9 v 388.2" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(0.2,0,0,-0.2,-111.1,21.9)" >
|
||||
<path style="fill: rgb(81, 173, 229); fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-dasharray: none; stroke-dashoffset: 0; stroke-miterlimit: 4; opacity: 1" paint-order="stroke" transform="translate(-1143.2,-1383.2)" d="M 1861.3 912.4 v -419.2 c 0 -5.8 4.7 -10.4 10.4 -10.4 c 5.7 0 10.4 4.7 10.4 10.4 v 419.2 c 0 5.8 -4.6 10.4 -10.4 10.4 c -5.7 0 -10.4 -4.7 -10.4 -10.4" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(0.2,0,0,-0.2,-103.4,18.6)" >
|
||||
<path style="fill: rgb(81, 173, 229); fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-dasharray: none; stroke-dashoffset: 0; stroke-miterlimit: 4; opacity: 1" paint-order="stroke" transform="translate(-1176.6,-1397.2)" d="M 1894.7 940.4 v -447.2 c 0 -5.8 4.7 -10.5 10.4 -10.5 c 5.7 0 10.4 4.7 10.4 10.5 v 447.2 c 0 5.8 -4.6 10.4 -10.4 10.4 c -5.7 0 -10.4 -4.7 -10.4 -10.4" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(0.2,0,0,-0.2,-95.7,17.5)" >
|
||||
<path style="fill: rgb(81, 173, 229); fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-dasharray: none; stroke-dashoffset: 0; stroke-miterlimit: 4; opacity: 1" paint-order="stroke" transform="translate(-1210,-1401.8)" d="M 1928.1 949.5 v -456.1 c 0 -5.9 4.6 -10.6 10.4 -10.6 c 5.7 0 10.4 4.8 10.4 10.6 v 456.1 c 0 5.9 -4.6 10.7 -10.4 10.7 c -5.7 0 -10.4 -4.8 -10.4 -10.7" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(0.2,0,0,-0.2,-88,18.6)" >
|
||||
<path style="fill: rgb(81, 173, 229); fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-dasharray: none; stroke-dashoffset: 0; stroke-miterlimit: 4; opacity: 1" paint-order="stroke" transform="translate(-1243.4,-1397.2)" d="M 1961.5 939.8 v -445.9 c 0 -6.1 4.6 -11.1 10.4 -11.1 c 5.7 0 10.4 5 10.4 11.1 v 445.9 c 0 6.1 -4.7 11.1 -10.4 11.1 c -5.7 0 -10.4 -5 -10.4 -11.1" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(0.2,0,0,-0.2,-80.3,21.9)" >
|
||||
<path style="fill: rgb(81, 173, 229); fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-dasharray: none; stroke-dashoffset: 0; stroke-miterlimit: 4; opacity: 1" paint-order="stroke" transform="translate(-1276.8,-1383.2)" d="M 1994.9 912.4 v -419.2 c 0 -5.8 4.6 -10.4 10.4 -10.4 c 5.7 0 10.4 4.7 10.4 10.4 v 419.2 c 0 5.8 -4.7 10.4 -10.4 10.4 c -5.7 0 -10.4 -4.7 -10.4 -10.4" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(0.2,0,0,-0.2,0,12.6)" >
|
||||
<path style="fill: rgb(64, 50, 44); fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-dasharray: none; stroke-dashoffset: 0; stroke-miterlimit: 4; opacity: 1" paint-order="stroke" transform="translate(-1625,-1423.3)" d="M 2262.7 878.7 c 0 52.1 44.1 94.1 97 90.6 c 48.2 -3.2 84.6 -45.5 84.6 -93.8 V 490.5 c 0 -14.3 11.6 -25.9 25.9 -25.9 c 14.3 0 25.9 11.6 25.9 25.9 v 384 c 0 76.7 -58.8 142.8 -135.5 146.6 c -81.9 4 -149.8 -61.4 -149.8 -142.5 V 490.5 c 0 -14.3 11.6 -25.9 25.9 -25.9 c 14.3 0 25.9 11.6 25.9 25.9 v 388.2" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(0.2,0,0,-0.2,-15.4,21.9)" >
|
||||
<path style="fill: rgb(191, 27, 44); fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-dasharray: none; stroke-dashoffset: 0; stroke-miterlimit: 4; opacity: 1" paint-order="stroke" transform="translate(-1558.2,-1383.2)" d="M 2276.3 912.4 v -419.2 c 0 -5.8 4.7 -10.4 10.4 -10.4 c 5.7 0 10.4 4.7 10.4 10.4 v 419.2 c 0 5.8 -4.6 10.4 -10.4 10.4 c -5.7 0 -10.4 -4.7 -10.4 -10.4" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(0.2,0,0,-0.2,-7.7,18.6)" >
|
||||
<path style="fill: rgb(191, 27, 44); fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-dasharray: none; stroke-dashoffset: 0; stroke-miterlimit: 4; opacity: 1" paint-order="stroke" transform="translate(-1591.6,-1397.2)" d="M 2309.7 940.4 v -447.2 c 0 -5.8 4.7 -10.5 10.4 -10.5 c 5.7 0 10.4 4.7 10.4 10.5 v 447.2 c 0 5.8 -4.6 10.4 -10.4 10.4 c -5.7 0 -10.4 -4.7 -10.4 -10.4" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(0.2,0,0,-0.2,0,17.5)" >
|
||||
<path style="fill: rgb(191, 27, 44); fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-dasharray: none; stroke-dashoffset: 0; stroke-miterlimit: 4; opacity: 1" paint-order="stroke" transform="translate(-1625,-1401.8)" d="M 2343.1 949.5 v -456.1 c 0 -5.9 4.6 -10.6 10.4 -10.6 c 5.7 0 10.4 4.8 10.4 10.6 v 456.1 c 0 5.9 -4.6 10.7 -10.4 10.7 c -5.7 0 -10.4 -4.8 -10.4 -10.7" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(0.2,0,0,-0.2,7.7,18.6)" >
|
||||
<path style="fill: rgb(191, 27, 44); fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-dasharray: none; stroke-dashoffset: 0; stroke-miterlimit: 4; opacity: 1" paint-order="stroke" transform="translate(-1658.4,-1397.2)" d="M 2376.5 939.8 v -445.9 c 0 -6.1 4.6 -11.1 10.4 -11.1 c 5.7 0 10.4 5 10.4 11.1 v 445.9 c 0 6.1 -4.7 11.1 -10.4 11.1 c -5.7 0 -10.4 -5 -10.4 -11.1" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(0.2,0,0,-0.2,15.4,21.9)" >
|
||||
<path style="fill: rgb(191, 27, 44); fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-dasharray: none; stroke-dashoffset: 0; stroke-miterlimit: 4; opacity: 1" paint-order="stroke" transform="translate(-1691.8,-1383.2)" d="M 2409.9 912.4 v -419.2 c 0 -5.8 4.6 -10.4 10.4 -10.4 c 5.7 0 10.4 4.7 10.4 10.4 v 419.2 c 0 5.8 -4.7 10.4 -10.4 10.4 c -5.7 0 -10.4 -4.7 -10.4 -10.4" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(0.2,0,0,-0.2,95.7,12.6)" >
|
||||
<path style="fill: rgb(64, 50, 44); fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-dasharray: none; stroke-dashoffset: 0; stroke-miterlimit: 4; opacity: 1" paint-order="stroke" transform="translate(-2040,-1423.3)" d="M 2677.7 878.7 c 0 52.1 44.1 94.1 97 90.6 c 48.2 -3.2 84.6 -45.5 84.6 -93.8 V 490.5 c 0 -14.3 11.6 -25.9 25.9 -25.9 c 14.3 0 25.9 11.6 25.9 25.9 v 384 c 0 76.7 -58.8 142.8 -135.5 146.6 c -81.9 4 -149.8 -61.4 -149.8 -142.5 V 490.5 c 0 -14.3 11.6 -25.9 25.9 -25.9 c 14.3 0 25.9 11.6 25.9 25.9 v 388.2" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(0.2,0,0,-0.2,80.3,21.9)" >
|
||||
<path style="fill: rgb(128, 204, 40); fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-dasharray: none; stroke-dashoffset: 0; stroke-miterlimit: 4; opacity: 1" paint-order="stroke" transform="translate(-1973.2,-1383.2)" d="M 2691.3 912.4 v -419.2 c 0 -5.8 4.7 -10.4 10.4 -10.4 c 5.7 0 10.4 4.7 10.4 10.4 v 419.2 c 0 5.8 -4.6 10.4 -10.4 10.4 c -5.7 0 -10.4 -4.7 -10.4 -10.4" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(0.2,0,0,-0.2,88,18.6)" >
|
||||
<path style="fill: rgb(128, 204, 40); fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-dasharray: none; stroke-dashoffset: 0; stroke-miterlimit: 4; opacity: 1" paint-order="stroke" transform="translate(-2006.6,-1397.2)" d="M 2724.7 940.4 v -447.2 c 0 -5.8 4.7 -10.5 10.4 -10.5 c 5.7 0 10.4 4.7 10.4 10.5 v 447.2 c 0 5.8 -4.6 10.4 -10.4 10.4 c -5.7 0 -10.4 -4.7 -10.4 -10.4" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(0.2,0,0,-0.2,95.7,17.5)" >
|
||||
<path style="fill: rgb(128, 204, 40); fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-dasharray: none; stroke-dashoffset: 0; stroke-miterlimit: 4; opacity: 1" paint-order="stroke" transform="translate(-2040,-1401.8)" d="M 2758.1 949.5 v -456.1 c 0 -5.9 4.6 -10.6 10.4 -10.6 c 5.7 0 10.4 4.8 10.4 10.6 v 456.1 c 0 5.9 -4.6 10.7 -10.4 10.7 c -5.7 0 -10.4 -4.8 -10.4 -10.7" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(0.2,0,0,-0.2,103.4,18.6)" >
|
||||
<path style="fill: rgb(128, 204, 40); fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-dasharray: none; stroke-dashoffset: 0; stroke-miterlimit: 4; opacity: 1" paint-order="stroke" transform="translate(-2073.4,-1397.2)" d="M 2791.5 939.8 v -445.9 c 0 -6.1 4.6 -11.1 10.4 -11.1 c 5.7 0 10.4 5 10.4 11.1 v 445.9 c 0 6.1 -4.7 11.1 -10.4 11.1 c -5.7 0 -10.4 -5 -10.4 -11.1" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(0.2,0,0,-0.2,111.1,21.9)" >
|
||||
<path style="fill: rgb(128, 204, 40); fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-dasharray: none; stroke-dashoffset: 0; stroke-miterlimit: 4; opacity: 1" paint-order="stroke" transform="translate(-2106.8,-1383.2)" d="M 2824.8 912.4 v -419.2 c 0 -5.8 4.6 -10.4 10.4 -10.4 c 5.7 0 10.4 4.7 10.4 10.4 v 419.2 c 0 5.8 -4.7 10.4 -10.4 10.4 c -5.7 0 -10.4 -4.7 -10.4 -10.4" stroke-linecap="round" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 11 KiB |
BIN
workspaces/base-workspace/mkdocs/docs/assets/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
16
workspaces/base-workspace/mkdocs/docs/javascript/config.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
window.MathJax = {
|
||||
tex: {
|
||||
inlineMath: [["\\(", "\\)"]],
|
||||
displayMath: [["\\[", "\\]"]],
|
||||
processEscapes: true,
|
||||
processEnvironments: true
|
||||
},
|
||||
options: {
|
||||
ignoreHtmlClass: ".*|",
|
||||
processHtmlClass: "arithmatex"
|
||||
}
|
||||
};
|
||||
|
||||
document$.subscribe(() => {
|
||||
MathJax.typesetPromise()
|
||||
})
|
99
workspaces/base-workspace/mkdocs/docs/pages/home/home.md
Normal file
|
@ -0,0 +1,99 @@
|
|||
<style>
|
||||
/* These styles apply only to this page! */
|
||||
.md-content__button {
|
||||
display: none;
|
||||
}
|
||||
.md-typeset h1 {
|
||||
line-height: 0;
|
||||
margin: 0;
|
||||
margin-left: -9999px;
|
||||
}
|
||||
.quickstart-wrapper {
|
||||
min-width: 300px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
padding-left: -50px;
|
||||
column-gap: 50px;
|
||||
row-gap: 50px;
|
||||
}
|
||||
.quickstart-wrapper > div {
|
||||
flex: 300px;
|
||||
max-width: 300px;
|
||||
}
|
||||
.tool-img{
|
||||
box-shadow: rgba(0, 0, 0, 0.24) 0px 5px 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.tool-caption{
|
||||
font-family: Roboto, Helvetica, sans-serif;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
|
||||
/* font-size: 1.25em;
|
||||
font-weight: 400; */
|
||||
letter-spacing: -.02em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.tool-description{
|
||||
font-family: Helvetica, sans-serif;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
font-size: 0.7rem;
|
||||
font-style: oblique;
|
||||
/* font-weight: bold; */
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
{%
|
||||
set tools = [
|
||||
{
|
||||
"env": "FILEBROWSER_URL",
|
||||
"name": "File Browser",
|
||||
"image": "Filebrowser.png",
|
||||
"description": "Browse files inside running docker container. Upload and download files and folders to and from your Workspace, no matter where your Workspace is running: on local or in cloud"
|
||||
},
|
||||
{
|
||||
"env": "CRONICLE_URL",
|
||||
"name": "Cronicle",
|
||||
"image": "Cronicle.png",
|
||||
"description": "Schedule jobs, tasks and bacground scripts. Powerful tool to manage schedules, observe and monitor executions."
|
||||
},
|
||||
{
|
||||
"env": "UNGIT_URL",
|
||||
"name": "Ungit",
|
||||
"image": "Ungit.png",
|
||||
"description": "Manage Git repositories and work flow using beautiful UI"
|
||||
},
|
||||
{
|
||||
"env": "STATICFS_URL",
|
||||
"name": "Static File Server",
|
||||
"image": "Static-server.png",
|
||||
"description": "Serve any static websites like a breeze"
|
||||
}
|
||||
]
|
||||
%}
|
||||
|
||||
|
||||
<div class="quickstart-wrapper">
|
||||
{% for tool in tools %}
|
||||
{% set tool_url = get_tool_url(tool.env) %}
|
||||
<div>
|
||||
<a href="{{ tool_url }}" target="_blank" rel="noopener noreferrer">
|
||||
<img src="{{ tool.image }}" class="tool-img"/>
|
||||
</a>
|
||||
<a href="{{ tool_url }}">
|
||||
<div class="tool-caption">{{ tool.name }}</div>
|
||||
</a>
|
||||
<div class="tool-description">{{ tool.description }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 870 KiB |
After Width: | Height: | Size: 303 KiB |
BIN
workspaces/base-workspace/mkdocs/docs/pages/home/home/MkDocs.png
Executable file
After Width: | Height: | Size: 652 KiB |
After Width: | Height: | Size: 169 KiB |
BIN
workspaces/base-workspace/mkdocs/docs/pages/home/home/Ungit.png
Normal file
After Width: | Height: | Size: 587 KiB |
49
workspaces/base-workspace/mkdocs/macros/helpers.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
"""
|
||||
Basic example of a Mkdocs-macros module.
|
||||
Include this {{ macros_info() }} in any page to get complete macro info
|
||||
"""
|
||||
import os
|
||||
|
||||
port_increments = {
|
||||
"DOCS_URL": 0,
|
||||
"FILEBROWSER_URL": 1,
|
||||
"STATICFS_URL": 2,
|
||||
"CRONICLE_URL": 3,
|
||||
"UNGIT_URL": 4}
|
||||
|
||||
|
||||
|
||||
# this function name should not be changed
|
||||
def define_env(env):
|
||||
"""
|
||||
This is the hook for defining variables, macros and filters
|
||||
- variables: the dictionary that contains the environment variables
|
||||
- macro: a decorator function, to declare a macro.
|
||||
- filter: a function with one of more arguments,
|
||||
used to perform a transformation
|
||||
"""
|
||||
@env.macro
|
||||
def get_tool_url(env):
|
||||
try:
|
||||
return os.environ[name]
|
||||
except:
|
||||
# Get host
|
||||
host = "localhost"
|
||||
try:
|
||||
host = os.environ["WRK_HOST"]
|
||||
except:
|
||||
pass
|
||||
# Entry port - port relative to which other ports will be calculated
|
||||
entry_port = 8020
|
||||
try:
|
||||
entry_port = int(os.environ["ENTRY_PORT"])
|
||||
except:
|
||||
pass
|
||||
# Assign port
|
||||
try:
|
||||
port = port_increments[env] + entry_port
|
||||
except:
|
||||
port = 80
|
||||
return f"http://{host}:{port}"
|
||||
|
||||
|
70
workspaces/base-workspace/mkdocs/mkdocs.yml
Normal file
|
@ -0,0 +1,70 @@
|
|||
# ===========================================================
|
||||
# NAVIGATION
|
||||
# ===========================================================
|
||||
|
||||
nav:
|
||||
- Home: pages/home/home.md
|
||||
- About: README.md
|
||||
- Get started: get-started.md
|
||||
|
||||
|
||||
# ===========================================================
|
||||
# CONFIGURATION
|
||||
# ===========================================================
|
||||
|
||||
site_name: Base Workspace
|
||||
repo_url: https://github.com/Alnoda/workspaces-in-docker/tree/main/workspaces/base-workspace
|
||||
site_url: https://alnoda.org
|
||||
edit_uri: ""
|
||||
|
||||
# ===========================================================
|
||||
# APPEARANCE
|
||||
# ===========================================================
|
||||
|
||||
theme:
|
||||
name: 'material'
|
||||
favicon: 'assets/favicon.ico'
|
||||
logo: 'assets/Alnoda-logo.svg'
|
||||
custom_dir: overrides
|
||||
icon:
|
||||
repo: fontawesome/brands/git-alt
|
||||
features:
|
||||
- navigation.instant
|
||||
palette:
|
||||
- scheme: default
|
||||
toggle:
|
||||
icon: material/toggle-switch-off-outline
|
||||
name: Switch to light mode
|
||||
primary: brown
|
||||
accent: deep orange
|
||||
- scheme: slate
|
||||
toggle:
|
||||
icon: material/toggle-switch
|
||||
name: Switch to dark mode
|
||||
primary: orange
|
||||
accent: red
|
||||
|
||||
extra:
|
||||
# Link to open when your logo is clicked
|
||||
homepage: https://alnoda.org
|
||||
host_url: http://localhost
|
||||
|
||||
plugins:
|
||||
- search
|
||||
# Enable Macros and jinja2 templates
|
||||
- macros:
|
||||
module_name: macros/helpers
|
||||
|
||||
|
||||
extra_javascript:
|
||||
- javascripts/config.js
|
||||
- https://polyfill.io/v3/polyfill.min.js?features=es6
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
44
workspaces/base-workspace/supervisord-workspace-base.conf
Executable file
|
@ -0,0 +1,44 @@
|
|||
[program:mkdocs]
|
||||
directory=/home/docs
|
||||
command=/bin/sh -c " mkdocs serve -a 0.0.0.0:8020 "
|
||||
stderr_logfile = /var/log/mkdocs/mkdocs-stderr.log
|
||||
stdout_logfile = /var/log/mkdocs/mkdocs-stdout.log
|
||||
logfile_maxbytes = 1024
|
||||
|
||||
|
||||
[program:filebrowser]
|
||||
directory=/opt/filebrowser
|
||||
command=/bin/sh -c " filebrowser "
|
||||
stderr_logfile = /var/log/filebrowser/filebrowser-stderr.log
|
||||
stdout_logfile = /var/log/filebrowser/filebrowser-stdout.log
|
||||
logfile_maxbytes = 1024
|
||||
|
||||
[program:serve]
|
||||
directory=/home/static-server
|
||||
command=/bin/sh -c " cd /opt/serve; . env/bin/activate; serve -p 8022 /home/static-server "
|
||||
stderr_logfile = /var/log/static-file-server/serve-stderr.log
|
||||
stdout_logfile = /var/log/static-file-server/serve-stdout.log
|
||||
logfile_maxbytes = 1024
|
||||
|
||||
[program:cronicle]
|
||||
directory=/opt/cronicle
|
||||
command=/bin/sh -c " rm /opt/cronicle/logs/cronicled.pid || true; cd /opt/cronicle; . env/bin/activate; /opt/cronicle/bin/control.sh setup; /opt/cronicle/bin/control.sh start "
|
||||
stderr_logfile = /var/log/cronicle/cronicle-stderr.log
|
||||
stdout_logfile = /var/log/cronicle/cronicle-stdout.log
|
||||
logfile_maxbytes = 1024
|
||||
exitcodes=0
|
||||
startsecs=0
|
||||
|
||||
[program:ungit]
|
||||
directory=/opt/ungit
|
||||
command=/bin/sh -c " cd /opt/ungit; . env/bin/activate; ungit --port=8024 --ungitBindIp=0.0.0.0 --launchBrowser=false --autoFetch=false --bugtracking=false --authentication=false "
|
||||
stderr_logfile = /var/log/ungit/ungit-stderr.log
|
||||
stdout_logfile = /var/log/ungit/ungit-stdout.log
|
||||
logfile_maxbytes = 1024
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
24
workspaces/codeserver-workspace/Dockerfile
Normal file
|
@ -0,0 +1,24 @@
|
|||
ARG docker_registry=docker.io/alnoda
|
||||
ARG image_tag=18.04-0.5
|
||||
|
||||
FROM ${docker_registry}/base-workspace:${image_tag}
|
||||
|
||||
USER root
|
||||
|
||||
COPY supervisord-codeserver.conf /etc/supervisord/
|
||||
|
||||
RUN echo "------------------------------------------------------ code-server" \
|
||||
&& mkdir -p -m 777 /opt/codeserver \
|
||||
&& cd /opt/codeserver && nodeenv --node=12.18.3 --npm=6.0.0 env \
|
||||
&& cd /opt/codeserver && . env/bin/activate && npm install -g --unsafe-perm code-server@3.10.2 \
|
||||
&& mkdir -p -m 777 /home/project \
|
||||
&& mkdir -p -m 777 /opt/codeserver/data \
|
||||
&& mkdir -p -m 777 /opt/codeserver/extensions \
|
||||
&& mkdir -p -m 777 /var/log/codeserver \
|
||||
&& echo "------------------------------------------------------ user" \
|
||||
&& chown -R abc /home/project \
|
||||
&& chown -R abc /opt/codeserver \
|
||||
&& chown -R abc /opt/codeserver/data \
|
||||
&& chown -R abc /opt/codeserver/extensions
|
||||
|
||||
COPY code-server-run.sh /opt/codeserver/code-server-run.sh
|
25
workspaces/codeserver-workspace/code-server-run.sh
Normal file
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/bash
|
||||
|
||||
if [ -n "${PASSWORD}" ] || [ -n "${HASHED_PASSWORD}" ]; then
|
||||
AUTH="password"
|
||||
else
|
||||
AUTH="none"
|
||||
echo "starting with no password"
|
||||
fi
|
||||
|
||||
if [ -z ${PROXY_DOMAIN+x} ]; then
|
||||
PROXY_DOMAIN_ARG=""
|
||||
else
|
||||
PROXY_DOMAIN_ARG="--proxy-domain=${PROXY_DOMAIN}"
|
||||
fi
|
||||
|
||||
export SHELL=/bin/zsh
|
||||
|
||||
code-server \
|
||||
--bind-addr 0.0.0.0:8025 \
|
||||
--user-data-dir /opt/vscode/data \
|
||||
--extensions-dir /opt/vscode/extensions \
|
||||
--disable-telemetry \
|
||||
--auth "${AUTH}" \
|
||||
"${PROXY_DOMAIN_ARG}" \
|
||||
/home/project
|
|
@ -0,0 +1,6 @@
|
|||
[program:codeserver]
|
||||
directory=/opt/codeserver
|
||||
command=/bin/sh -c " cd /opt/codeserver; . env/bin/activate; bash code-server-run.sh "
|
||||
stderr_logfile = /var/log/codeserver/serve-stderr.log
|
||||
stdout_logfile = /var/log/codeserver/serve-stdout.log
|
||||
logfile_maxbytes = 1024
|
23
workspaces/mkdocs-magicspace/Dockerfile
Normal file
|
@ -0,0 +1,23 @@
|
|||
ARG docker_registry=docker.io/alnoda
|
||||
ARG image_tag=18.04-03
|
||||
|
||||
FROM ${docker_registry}/workspace-in-docker:${image_tag}
|
||||
|
||||
USER root
|
||||
|
||||
# Change Theia color theme for this magic workspace to stand out
|
||||
COPY settings.json /home/abc/.theia/settings.json
|
||||
# More dependencies for mkdocs and markdown
|
||||
COPY mkdocs-requirements.txt /home/abc/installed-python-packages
|
||||
|
||||
RUN apt-get -y update \
|
||||
&& echo "-------------------------------------------- weasyprint" \
|
||||
&& apt-get install -y build-essential python3-dev python3-pip python3-setuptools python3-wheel python3-cffi libcairo2 libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 libffi-dev shared-mime-info \
|
||||
&& echo "-------------------------------------------- mkdocs plugins" \
|
||||
&& pip install -r /home/abc/installed-python-packages/mkdocs-requirements.txt
|
||||
|
||||
USER abc
|
||||
|
||||
# Custom docs for this workspace
|
||||
COPY mkdocs /home/docs
|
||||
COPY README.md /home/docs/docs/get-started.md
|