diff --git a/.travis.yml b/.travis.yml index d04ca34..57e75c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ services: - docker install: + - cp config/config.yml.example config/config.yml - docker build -t automatron . - docker run -d -p 127.0.0.1:6378:6378 --name redis redis - docker run -d --link redis:redis --name automatron automatron @@ -19,7 +20,8 @@ before_script: script: - docker ps - - python tests.py + - docker logs automatron + - docker run --rm=True --link automatron:automatron --link redis:redis automatron python tests.py -i -f - coverage run tests.py after_success: diff --git a/Procfile b/Procfile index 12c9a06..36ce3a6 100644 --- a/Procfile +++ b/Procfile @@ -2,3 +2,4 @@ discovery: python -B discovery.py -c config/config.yml runbooks: python -B runbooks.py -c config/config.yml monitoring: python -B monitoring.py -c config/config.yml actioning: python -B actioning.py -c config/config.yml +web: python -B web.py -c config/config.yml diff --git a/README.md b/README.md index c5b2b29..a20f95b 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ Automatron is a framework for creating self-healing infrastructure. Simply put, The goal of Automatron is to allow users to automate the execution of common tasks performed during system events. These tasks can be as simple as **sending an email** to as complicated as **restarting services across multiple hosts**. +![Automatron Dashboard](https://raw.githubusercontent.com/madflojo/automatron/develop/docs/img/dashboard.png) + ## Features * Automatically detect and add new systems to monitor diff --git a/actioning.py b/actioning.py index 7139f38..e7118e7 100644 --- a/actioning.py +++ b/actioning.py @@ -72,11 +72,16 @@ def update_target_status(item, target): 'CRITICAL' : 0, 'UNKNOWN' : 0 } + target['runbooks'][item['runbook']]['last_status'] = None # Increase counts of status' for check in item['checks'].keys(): + # Update Status Counter target['runbooks'][item['runbook']]['status'][item['checks'][check]] = ( target['runbooks'][item['runbook']]['status'][item['checks'][check]] + 1) - not_found.discard(item['checks'][check]) # Removes status from list of not found status' + # Update last_status with return + target['runbooks'][item['runbook']]['last_status'] = item['checks'][check] + # Removes status from list of not found status' + not_found.discard(item['checks'][check]) # Reset count to 0 for missing status' for missing_status in not_found: diff --git a/config/config.yml.example b/config/config.yml.example index 9a3e27d..863b78d 100644 --- a/config/config.yml.example +++ b/config/config.yml.example @@ -34,7 +34,7 @@ discovery: # Web Service for HTTP PINGs webping: ip: 0.0.0.0 - port: 8000 + port: 9000 # roster: # hosts: # - 10.0.0.1 @@ -50,3 +50,10 @@ datastore: db: 0 host: redis port: 6379 + +## Web UI & API +web: + listen: 0.0.0.0 + port: 8000 + # Bootswatch theme - https://bootswatch.com/ + theme: slate diff --git a/config/runbooks/init.yml b/config/runbooks/init.yml index c5f534b..e0d82d3 100644 --- a/config/runbooks/init.yml +++ b/config/runbooks/init.yml @@ -1,4 +1,4 @@ -'*': +#'*': ## Example Runbooks ## ---------------- # - examples/disk_free diff --git a/docker-compose.yml b/docker-compose.yml index fe22868..8d050ab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,13 +2,21 @@ version: '2' services: automatron: build: . + ports: + - 8000:8000 + volumes: + - ./templates:/templates + - ./static:/static depends_on: - redis redis: image: redis test: build: . - command: python /tests.py + command: python /tests.py -i -f + depends_on: + - redis + - automatron mkdocs: build: context: . @@ -16,8 +24,8 @@ services: volumes: - ./:/tmp/mkdocs ports: - - 8000:8000 - command: mkdocs serve -a 0.0.0.0:8000 + - 8080:8080 + command: mkdocs serve -a 0.0.0.0:8080 coverage: build: . command: coverage run tests.py && coverage report diff --git a/docs/img/dashboard.png b/docs/img/dashboard.png new file mode 100644 index 0000000..5399ad3 Binary files /dev/null and b/docs/img/dashboard.png differ diff --git a/docs/index.md b/docs/index.md index 35a5b1e..5e8700a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,9 +1,9 @@ -![Automatron](https://raw.githubusercontent.com/madflojo/automatron/master/docs/img/logo_huge.png) - Automatron is a framework for creating self-healing infrastructure. Simply put, it detects system events & takes action to correct them. The goal of Automatron is to allow users to automate the execution of common tasks performed during system events. These tasks can be as simple as **sending an email** to as complicated as **restarting services across multiple hosts**. +![Automatron Dashboard](/img/dashboard.png) + ## Features * Automatically detect and add new systems to monitor @@ -13,6 +13,8 @@ The goal of Automatron is to allow users to automate the execution of common tas * Allows dead simple **arbitrary shell commands** for both [checks](runbooks/checks.md) and [actions](runbooks/actions.md) * Runbook flexibility with **Jinja2** templating support * Pluggable Architecture that simplifies customization + * Bootstrap based dashboard showing real-time events + ## Runbooks @@ -94,4 +96,4 @@ By using **Facts** and **Jinja2** together you can customize a single runbook to ## Follow Automatron -[![Twitter Follow](https://img.shields.io/twitter/follow/automatronio.svg?style=flat-square)](https://twitter.com/automatronio) [![Gitter](https://badges.gitter.im/madflojo/automatron.svg)](https://gitter.im/madflojo/automatron?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![GitHub forks](https://img.shields.io/github/forks/madflojo/automatron.svg?style=social&label=Fork)](https://github.com/madflojo/automatron) [![GitHub stars](https://img.shields.io/github/stars/madflojo/automatron.svg?style=social&label=Star)](https://github.com/madflojo/automatron) +[![Twitter Follow](https://img.shields.io/twitter/follow/automatronio.svg?style=flat-square)](https://twitter.com/automatronio) [![Gitter](https://badges.gitter.im/madflojo/automatron.svg)](https://gitter.im/madflojo/automatron?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![GitHub stars](https://img.shields.io/github/stars/madflojo/automatron.svg?style=social&label=Star)](https://github.com/madflojo/automatron) diff --git a/docs/install/docker.md b/docs/install/docker.md index 76dab8a..a509689 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -15,12 +15,16 @@ The above `redis` instance will be used as a default datastore for Automatron. Once the `redis` instance is up and running we can start an Automatron instance. ```sh -$ sudo docker run -d --link redis:redis -p 8000:8000 -v /path/to/config:/config --restart=always --name automatron madflojo/automatron +$ sudo docker run -d --link redis:redis -p 8000:8000 -p 9000:9000 -v /path/to/config:/config --restart=always --name automatron madflojo/automatron ``` In the above `docker run` command we are using `-v` to mount a directory from the host to the container as `/config`. This `/config` directory will be the home to Automatron's configuration files and Runbooks. +## Dashboard + +To view the Automatron dashboard simply open up `http://:8080` in your favorite browser. As target nodes are identified and runbooks are executed, events will start to be reflected on the dashboard. + With these steps complete, we can now move to [Configuring](/configure.md) Automatron. -!!! tip +!!! tip A `docker-compose.yml` file is included in the base repository which can be used to quickly stand up environments using `docker-compose up automatron`. diff --git a/docs/install/index.md b/docs/install/index.md index c97decc..36efdf2 100644 --- a/docs/install/index.md +++ b/docs/install/index.md @@ -62,3 +62,7 @@ $ honcho start ``` To shut down Automatron you can use the `kill` command to send the `SIGTERM` signal to the running processes. + +## Dashboard + +To view the Automatron dashboard simply open up `http://:8080` in your favorite browser. As target nodes are identified and runbooks are executed, events will start to be reflected on the dashboard. diff --git a/mkdocs.yml b/mkdocs.yml index c5f87e5..d509392 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,6 +7,9 @@ site_description: A framework for creating self-healing infrastructure. It simpl site_author: Benjamin Cane site_favicon: img/favicon.ico theme: material +google_analytics: + - "UA-3378969-14" + - "auto" extra: logo: 'img/automatron.png' social: diff --git a/plugins/vetting/remote/.empty b/plugins/vetting/remote/.empty deleted file mode 100644 index e69de29..0000000 diff --git a/plugins/vetting/remote/ping.sh b/plugins/vetting/remote/ping.sh new file mode 100644 index 0000000..33bdb7d --- /dev/null +++ b/plugins/vetting/remote/ping.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +## Ping server then return true or false JSON + +if [ -z $1 ] +then + exit 1 +fi + +/bin/ping -c 1 $1 > /dev/null 2>&1 +if [ $? -eq 0 ] +then + echo '{"ping": true}' +else + echo '{"ping": false}' +fi diff --git a/requirements.txt b/requirements.txt index 6794ef8..d08751d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ mock six>=1.7 coverage boto3 +flask diff --git a/runbooks.py b/runbooks.py index 0e3f355..2c30e76 100644 --- a/runbooks.py +++ b/runbooks.py @@ -51,16 +51,19 @@ def cache_runbooks(config, logger): if runbooks: for target in runbooks: all_books[target] = {} - for books in runbooks[target]: - logger.debug("Processing book: {0}".format(books)) - book_path = "{0}/{1}".format(config['runbook_path'], books) - if os.path.isdir(book_path): - book_path = book_path + "/init.yml" - if os.path.isfile(book_path) is False: - logger.warn("Runbook File Error: {0} is not a file".format(book_path)) - else: - with open(book_path) as bh: - all_books[target][books] = bh.read() + if runbooks[target]: + for books in runbooks[target]: + logger.debug("Processing book: {0}".format(books)) + book_path = "{0}/{1}".format(config['runbook_path'], books) + if os.path.isdir(book_path): + book_path = book_path + "/init.yml" + if os.path.isfile(book_path) is False: + logger.warn("Runbook File Error: {0} is not a file".format(book_path)) + else: + with open(book_path) as bh: + all_books[target][books] = bh.read() + else: + logger.warn("Error: No runbooks specified under {0}".format(target)) return all_books def render_runbooks(runbook, facts): diff --git a/static/img/automatron.png b/static/img/automatron.png new file mode 100644 index 0000000..16ec08e Binary files /dev/null and b/static/img/automatron.png differ diff --git a/static/img/banner.jpg b/static/img/banner.jpg new file mode 100755 index 0000000..4b011e4 Binary files /dev/null and b/static/img/banner.jpg differ diff --git a/static/img/logo.png b/static/img/logo.png new file mode 100644 index 0000000..edd594c Binary files /dev/null and b/static/img/logo.png differ diff --git a/static/js/automatron.js b/static/js/automatron.js new file mode 100644 index 0000000..6100626 --- /dev/null +++ b/static/js/automatron.js @@ -0,0 +1,205 @@ +/* + +Automatron: UI javascript + + - Pull Target info from API and update `div` + - Pull Runbook status from API and update `div` + - Pull Events from API and update `div` + +*/ + + +// Update Target list +function updateTargets(target) { + "use strict"; + var status = "default"; + var hostname = target.hostname.split("."); + var counts = { + "OK": 0, + "WARNING": 0, + "CRITICAL": 0, + "UNKNOWN": 0 + }; + + $.each(target.runbooks, function (key, value) { + // Check status and set bootstrap class accordingly + if (value.hasOwnProperty('status')) { + if (value.status.OK > 0 && status === "default") { + status = "success"; + } + if (value.status.WARNING > 0) { + status = "warning"; + } + if (value.status.CRITICAL > 0) { + status = "danger"; + } + $.each(value.status, function (k, v) { + if (v > 0) { + counts[k] = counts[k] + 1; + } + }); + } + }); + + $("div.servers-group").append("
\ +

" + hostname[0] + "


\ +

" + target.hostname + " " + target.facts.os + "


\ + " + counts.OK + " OK \ + " + counts.WARNING + " WARNING \ + " + counts.CRITICAL + " CRITICAL \ + " + counts.UNKNOWN + " UNKNOWN \ +
").fadeIn("slow"); +} + +// Update Target Counter +function updateTargetCount(count) { + "use strict"; + $("div.target-count").text(count).fadeIn("slow"); +} + +// Update Runbook OK Counter +function updateOkCount(count) { + "use strict"; + $("div.ok-count").text(count).fadeIn("slow"); +} + +// Update Runbook Unknown Counter +function updateUnknownCount(count) { + "use strict"; + $("div.unknown-count").text(count).fadeIn("slow"); +} + +// Update Runbook Warning Counter +function updateWarningCount(count) { + "use strict"; + $("div.warning-count").text(count).fadeIn("slow"); +} + +// Update Runbook Critical Counter +function updateCriticalCount(count) { + "use strict"; + $("div.critical-count").text(count).fadeIn("slow"); +} + +// Add Runbook Event to event stream +function addEvent(value) { + "use strict"; + var hostname = value.target.split("."); + if (value.status === "CRITICAL") { + $("ul.status-group").append("
  • " + value.status + "" + hostname[0] + ": " + value.runbook + "
  • ").fadeIn("slow"); + } + if (value.status === "OK" && value.count <= 1) { + $("ul.status-group").append("
  • " + value.status + "" + hostname[0] + ": " + value.runbook + "
  • ").fadeIn("slow"); + } + if (value.status === "WARNING") { + $("ul.status-group").append("
  • " + value.status + "" + hostname[0] + ": " + value.runbook + "
  • ").fadeIn("slow"); + } + if (value.status === "UNKNOWN") { + $("ul.status-group").append("
  • " + value.status + "" + hostname[0] + ": " + value.runbook + "
  • ").fadeIn("slow"); + } +} + +// Perform API call and process results for Status and Events +function getStatus() { + "use strict"; + $.ajax({ + url: "/api/status", + dataType: "json", + success: function (json) { + updateTargetCount(json.targets); + updateOkCount(json.runbooks.OK); + updateWarningCount(json.runbooks.WARNING); + updateCriticalCount(json.runbooks.CRITICAL); + updateUnknownCount(json.runbooks.UNKNOWN); + // Clear existing status + if (json.events.length >= 1) { + $("ul.status-group").empty(); + $.each(json.events, function (key, value) { + addEvent(value); + }); + if ($("li.status-item").length === 0) { + $("ul.status-group").append("
  • No events found
  • "); + } + } else { + $("ul.status-group").empty(); + $("ul.status-group").append("
  • No events found
  • "); + } + } + }); +} + +// Perform API call and process results for Target list +function getTargets() { + "use strict"; + $.ajax({ + url: "/api/targets", + dataType: "json", + success: function (json) { + $("div.servers-group").empty(); + var added = 0; + $("div.servers-group").append("
    "); + $.each(json, function (key, value) { + if (added % 3 === 0) { + $("div.servers-group").append("
    "); + } + updateTargets(value); + added = added + 1; + }); + $("div.servers-group").append("
    "); + } + }); +} + + +$(document).on("click", ".server-info", function() { + "use strict"; + var name = $(this).data().hostname; + $("h4.modal-title").text(name); + $.ajax({ + url: "/api/targets/" + name, + dataType: "json", + success: function (json) { + // Update Server info + $("div.server-panel-info").html("
    " + + "
    Name: " + json.hostname + "
    Operating System: " + json.facts.os + "
    " + + "
    Address: " + json.ip + "
    Kernel: " + json.facts.kernel + "
    " + + "
    ") + + // Update Runbook status + $("ul.server-runbooks-group").empty(); + $.each(json.runbooks, function (key, value) { + if (value.last_status === "CRITICAL") { + $("ul.server-runbooks-group").append("
  • " + value.last_status + "" + value.name + "
  • "); + } + if (value.last_status === "OK") { + $("ul.server-runbooks-group").append("
  • " + value.last_status + "" + value.name + "
  • "); + } + if (value.last_status === "WARNING") { + $("ul.server-runbooks-group").append("
  • " + value.last_status + "" + value.name + "
  • "); + } + if (value.last_status === "UNKNOWN") { + $("ul.server-runbooks-group").append("
  • " + value.last_status + "" + value.name + "
  • "); + } + }); + if (json.runbooks.length < 0) { + $("ul.server-runbooks-group").append("
  • No Runbooks found
  • ") + } + } + }); +}); + + +// Wrapper run function +function run() { + "use strict"; + getStatus(); + getTargets(); +} + + +// Run run() on load and every 10 seconds +window.onload = run(); +window.setInterval(function () { + "use strict"; + run(); +}, 10000); diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..a92c888 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,120 @@ + + + + + + + Automatron: Dashboard + + + + + + +

    +
    + +
    +
    + Automatron Logo +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    + +
    +
    + Status: +
    +
      +
    • No events found
    • +
    +
    +
    + +
    +
    + + + + + + + + + + + diff --git a/tests.py b/tests.py index f17edbf..78cbbf2 100644 --- a/tests.py +++ b/tests.py @@ -4,8 +4,9 @@ import unittest import sys +import argparse -def run_unittests(): +def run_unit_tests(): ''' Execute Unit Tests ''' tests = unittest.TestLoader().discover('tests/unit') result = unittest.TextTestRunner(verbosity=2).run(tests) @@ -24,10 +25,22 @@ def run_functional_tests(): return result.wasSuccessful() if __name__ == '__main__': - unit_results = run_unittests() - integration_results = run_integration_tests() - functional_results = run_functional_tests() - if unit_results and integration_results and functional_results: + parser = argparse.ArgumentParser(description="Automatron Test Runner") + parser.add_argument('-i', '--integration', help="Run integration tests", action="store_true", required=False) + parser.add_argument('-f', '--functional', help="Run functional tests", action="store_true", required=False) + args = parser.parse_args() + + unit = run_unit_tests() + functional = True + integration = True + + if args.integration: + integration = run_integration_tests() + + if args.functional: + functional = run_functional_tests() + + if unit and integration and functional: sys.exit(0) else: sys.exit(1) diff --git a/tests/functional/TODO.md b/tests/functional/TODO.md deleted file mode 100644 index 6ad1722..0000000 --- a/tests/functional/TODO.md +++ /dev/null @@ -1 +0,0 @@ -Write tests! diff --git a/tests/functional/test_web_api.py b/tests/functional/test_web_api.py new file mode 100644 index 0000000..959fcd5 --- /dev/null +++ b/tests/functional/test_web_api.py @@ -0,0 +1,86 @@ +''' +Test API Functionality +''' + +import mock +import unittest + +import json +import redis +import requests + +class APITest(unittest.TestCase): + ''' Run unit tests against the API Methods ''' + + def setUp(self): + ''' Setup mocked data ''' + self.dbc = redis.Redis( + host="redis", + port=6379, + password=None, + db=0) + + def tearDown(self): + ''' Destroy mocked data ''' + self.dbc.flushdb() + +class TargetswithoutTargetSpecified(APITest): + ''' Test /api/targets when no target is specified ''' + def runTest(self): + ''' Execute test ''' + target = { + 'facts': {}, + 'runbooks' : {}, + 'hostname' : 'host1.example.com', + 'ip' : '10.0.0.1' + } + key = "targets:details:{0}".format(target['hostname']) + value = json.dumps(target) + self.dbc.set(key, value) + self.dbc.sadd('targets:ip:list', target['ip']) + self.dbc.set('targets:ip:details:' + target['ip'], target['hostname']) + + r = requests.get(url="http://automatron:8000/api/targets") + self.assertEqual(r.text, json.dumps({'host1.example.com' : target})) + self.assertEqual(r.status_code, 200) + +class TargetswithTargetSpecified(APITest): + ''' Test /api/targets when a target is specified ''' + def runTest(self): + ''' Execute test ''' + target = { + 'facts': {}, + 'runbooks' : {}, + 'hostname' : 'host1.example.com', + 'ip' : '10.0.0.1' + } + key = "targets:details:{0}".format(target['hostname']) + value = json.dumps(target) + self.dbc.set(key, value) + self.dbc.sadd('targets:ip:list', target['ip']) + self.dbc.set('targets:ip:details:' + target['ip'], target['hostname']) + + r = requests.get(url="http://automatron:8000/api/targets/host1.example.com") + self.assertEqual(r.text, json.dumps(target)) + self.assertEqual(r.status_code, 200) + +class Status(APITest): + ''' Test /api/status ''' + def runTest(self): + ''' Execute test ''' + target = { + 'facts': {}, + 'runbooks' : {}, + 'hostname' : 'host1.example.com', + 'ip' : '10.0.0.1' + } + key = "targets:details:{0}".format(target['hostname']) + value = json.dumps(target) + self.dbc.set(key, value) + self.dbc.sadd('targets:ip:list', target['ip']) + self.dbc.set('targets:ip:details:' + target['ip'], target['hostname']) + + r = requests.get(url="http://automatron:8000/api/status") + self.assertEqual(r.status_code, 200) + ret = json.loads(r.text) + self.assertEqual(ret['targets'], 1) diff --git a/tests/unit/test_actioning_update_target_status.py b/tests/unit/test_actioning_update_target_status.py index 51f87db..5584bf6 100644 --- a/tests/unit/test_actioning_update_target_status.py +++ b/tests/unit/test_actioning_update_target_status.py @@ -68,6 +68,7 @@ def runTest(self): }) returned_target = update_target_status(self.item, self.target) self.assertTrue(returned_target['runbooks']['book1']['status']['CRITICAL'] == 1) + self.assertTrue(returned_target['runbooks']['book1']['last_status'] == "CRITICAL") self.assertTrue(returned_target['runbooks']['book1']['status']['OK'] == 0) self.assertTrue(returned_target['runbooks']['book1']['status']['WARNING'] == 0) self.assertTrue(returned_target['runbooks']['book1']['status']['UNKNOWN'] == 0) @@ -85,3 +86,4 @@ def runTest(self): self.assertTrue(returned_target['runbooks']['book1']['status']['CRITICAL'] == 0) self.assertTrue(returned_target['runbooks']['book1']['status']['WARNING'] == 0) self.assertTrue(returned_target['runbooks']['book1']['status']['UNKNOWN'] == 0) + self.assertTrue(returned_target['runbooks']['book1']['last_status'] == "OK") diff --git a/tests/unit/test_runbooks_cache_runbooks.py b/tests/unit/test_runbooks_cache_runbooks.py index 6c9f248..ff01420 100644 --- a/tests/unit/test_runbooks_cache_runbooks.py +++ b/tests/unit/test_runbooks_cache_runbooks.py @@ -51,7 +51,7 @@ def runTest(self, mock_template, mock_yaml, mock_isfile, mock_open): self.config = mock.MagicMock(spec_set={'runbook_path' : '/path/'}) mock_isfile.return_value = True mock_yaml.return_value = None - mock_Template = mock.MagicMock(**{ + mock_template = mock.MagicMock(**{ 'render.return_value' : "" }) mock_open.return_value = mock.MagicMock(spec=file) @@ -59,6 +59,25 @@ def runTest(self, mock_template, mock_yaml, mock_isfile, mock_open): self.assertTrue(mock_open.called) self.assertTrue(mock_yaml.called) +class RunwithNoBooks(CacheRunbooksTest): + ''' Test with a partially created init.yml file ''' + @mock.patch('runbooks.open', create=True) + @mock.patch('runbooks.os.path.isfile') + @mock.patch('runbooks.yaml.load') + @mock.patch('runbooks.Template') + def runTest(self, mock_template, mock_yaml, mock_isfile, mock_open): + ''' Execute test ''' + self.config = mock.MagicMock(spec_set={'runbook_path' : '/path/'}) + mock_isfile.return_value = True + mock_yaml.return_value = None + mock_template = mock.MagicMock(**{ + 'render.return_value' : { '*': None } + }) + mock_open.return_value = mock.MagicMock(spec=file) + self.assertEqual(cache_runbooks(self.config, self.logger), {}) + self.assertTrue(mock_open.called) + self.assertTrue(mock_yaml.called) + class RunwithYMLFile(CacheRunbooksTest): ''' Test with a valid YML file ''' @mock.patch('runbooks.open', create=True) @@ -66,12 +85,12 @@ class RunwithYMLFile(CacheRunbooksTest): @mock.patch('runbooks.os.path.isdir') @mock.patch('runbooks.yaml.load') @mock.patch('runbooks.Template') - def runTest(self, mock_Template, mock_yaml, mock_isdir, mock_isfile, mock_open): + def runTest(self, mock_template, mock_yaml, mock_isdir, mock_isfile, mock_open): ''' Execute test ''' self.config = mock.MagicMock(spec_set={'runbook_path' : '/path/'}) mock_isfile.return_value = True mock_isdir.return_value = True - mock_Template = mock.MagicMock(**{ + mock_template = mock.MagicMock(**{ 'render.return_value' : """ '*': - book diff --git a/web.py b/web.py new file mode 100644 index 0000000..cedd489 --- /dev/null +++ b/web.py @@ -0,0 +1,103 @@ +''' + +Automatron: API Service + * Act as backend to user interface + * Provide REST API for Automatron + +''' + +import json +import sys +from flask import Flask, g, request, render_template +import core.common +import core.logs +import core.db + +app = Flask(__name__) + +def connect_db(config): + ''' Connect to DB ''' + db = core.db.SetupDatastore(config=config) + try: + return db.get_dbc() + except Exception as e: + logger.error("Failed to connect to datastore: {0}".format(e.message)) + return None + +@app.before_request +def before_request(): + ''' Pre-request hander ''' + logger.debug("Incoming Web Request: {0}".format(request.full_path)) + g.dbc = connect_db(app.config) + +@app.teardown_request +def teardown_request(exc=None): + ''' Post request handler ''' + if hasattr(g, 'dbc'): + g.dbc.disconnect() + +# Web UI Routes +@app.route('/', methods=['GET']) +def get_index(): + ''' Returns dashboard ''' + data = { + 'theme': 'slate' + } + # Set Bootswatch theme + if 'theme' in app.config['web']: + data['theme'] = app.config['web']['theme'] + return render_template("index.html", data=data), 200 + +# API Routes +@app.route('/api/status', methods=['GET']) +def get_status(): + ''' Returns JSON string summarizing current Automatron status ''' + status = { + 'targets': 0, + 'runbooks': { + 'OK': 0, + 'CRITICAL': 0, + 'WARNING': 0, + 'UNKNOWN': 0 + }, + 'events': [] + } + targets = g.dbc.get_target() + for target in targets.keys(): + status['targets'] = status['targets'] + 1 + if 'runbooks' in targets[target]: + for runbook in targets[target]['runbooks'].keys(): + if 'status' in targets[target]['runbooks'][runbook]: + for s in targets[target]['runbooks'][runbook]['status'].keys(): + if targets[target]['runbooks'][runbook]['status'][s] > 0: + status['runbooks'][s] = status['runbooks'][s] + 1 + status['events'].append({ + 'status': s, + 'target': target, + 'runbook': targets[target]['runbooks'][runbook]['name'], + 'count': targets[target]['runbooks'][runbook]['status'][s] + }) + return json.dumps(status), 200 + +@app.route('/api/targets/', methods=['GET']) +@app.route('/api/targets', methods=['GET']) +def get_targets(target=None): + ''' Returns JSON string of target information from the database ''' + targets = g.dbc.get_target(target_id=target) + return json.dumps(targets), 200 + +if __name__ == '__main__': + config = core.common.get_config(description="Automatron: Web") + if config is False: + print "Could not get configuration" + sys.exit(1) + app.config.update(config) + + # Setup Logging + if app.config['logging']['debug']: + app.debug = True + logs = core.logs.Logger(config=config, proc_name="api") + logger = logs.getLogger() + + # Start Flask + app.run(host=app.config['web']['listen'], port=app.config['web']['port'])