├── .bowerrc
├── .gitignore
├── .travis.yml
├── LICENSE
├── MANIFEST.in
├── README.md
├── bin
├── hermes
├── hermes-notify
└── hermes-server
├── bower.json
├── config
├── dev-client.yaml
└── dev.yaml
├── db
├── bootstrap.sql
├── truncate_tables.sql
├── update_to_046.sql
├── update_to_05.sql
├── update_to_051.sql
├── update_to_0511.sql
├── update_to_06.sql
├── update_to_0726.sql
└── update_to_0_5_15.sql
├── docs
├── Makefile
├── api-ref.rst
├── api.rst
├── conf.py
├── config.rst
├── index.rst
└── intro.rst
├── examples
└── ex1
│ ├── create-quest
│ ├── reboot-complete
│ ├── reboot-complete-remaining
│ └── servers.list
├── gulpfile.js
├── hermes
├── __init__.py
├── app.py
├── exc.py
├── handlers
│ ├── __init__.py
│ ├── api.py
│ ├── frontends.py
│ └── util.py
├── models.py
├── plugin.py
├── plugins
│ ├── __init__.py
│ └── hooks
│ │ └── __init__.py
├── routes.py
├── settings.py
├── settings_client.py
├── util.py
├── version.py
└── webapp
│ └── src
│ ├── css
│ ├── main.css
│ └── main.less
│ ├── img
│ ├── icon.gif
│ ├── loading.gif
│ ├── loading_15.gif
│ ├── logo.png
│ ├── remove_10.png
│ ├── remove_15.png
│ └── send_email_15.png
│ ├── index.html
│ ├── js
│ ├── controllers
│ │ ├── laborStatusCtrl.js
│ │ ├── questCreationCtrl.js
│ │ ├── questEditCtrl.js
│ │ ├── questStatusCtrl.js
│ │ └── userHomeCtrl.js
│ ├── directives
│ │ ├── boxContinuation.js
│ │ ├── fateGraph.js
│ │ ├── questProgressBar.js
│ │ ├── questProgressChart.js
│ │ ├── scrollWatch.js
│ │ └── viewportHeight.js
│ ├── filters
│ │ ├── encode.js
│ │ └── num.js
│ ├── hermesApp.js
│ └── services
│ │ ├── hermesService.js
│ │ └── skipReload.js
│ └── templates
│ ├── fateViewer.html
│ ├── laborList.html
│ ├── questCreation.html
│ ├── questEdit.html
│ ├── questStatus.html
│ └── userHome.html
├── npm-shrinkwrap.json
├── package.json
├── requirements-dev.txt
├── requirements.txt
├── setup.py
└── tests
├── api_tests
├── __init__.py
├── data
│ └── set1
│ │ ├── event.json
│ │ ├── eventtypes.json
│ │ ├── fates.json
│ │ └── hosts.json
├── fixtures.py
├── test_events.py
├── test_eventtypes.py
├── test_fates.py
├── test_hosts.py
├── test_labors.py
├── test_quests.py
└── util.py
├── model_tests
├── __init__.py
├── fixtures.py
├── test_events.py
├── test_eventtypes.py
├── test_fates.py
├── test_host.py
├── test_labors.py
└── test_quests.py
└── sample_data
├── sample_data1.sql
└── sample_data2.sql
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "analytics": false,
3 | "directory": "_bc"
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | *.pyc
3 | *.sqlite
4 | *.sqlite-journal
5 |
6 | # Bower Components
7 | _bc
8 |
9 | __pycache__
10 |
11 | MANIFEST
12 | *.py[cod]
13 |
14 | # C extensions
15 | *.so
16 |
17 | # Packages
18 | *.egg
19 | *.egg-info
20 | dist
21 | build
22 | eggs
23 | parts
24 | var
25 | sdist
26 | develop-eggs
27 | .installed.cfg
28 | lib
29 | lib64
30 |
31 | # Installer logs
32 | pip-log.txt
33 |
34 | # Unit test / coverage reports
35 | .coverage
36 | .tox
37 | nosetests.xml
38 |
39 | # Translations
40 | *.mo
41 |
42 | # Mr Developer
43 | .mr.developer.cfg
44 | .project
45 | .pydevproject
46 |
47 | docs/_build
48 | .*sw?
49 |
50 | # Node
51 | /npm-debug.log
52 | /node_modules
53 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - '2.7'
4 | install:
5 | - pip install -r requirements-dev.txt
6 | - pip install .
7 | script: py.test -vvv tests/
8 | after_success: curl -X POST http://readthedocs.org/build/dbx_hermes
9 | notifications:
10 | slack:
11 | secure: q2LtdP0ATZOFPZG2vI4CS1mCL+jZKVImzCq8mdOX3iM2FkIVKo4YxT/bNUJiWb4xOrenpuFz7J8scbY0gu3e3lgUGTSWFjQc9W6NEvgGKImSunqkF5vKi1VkAvFuYTbBxW6kQpRadTyRrcmVrK2OoBDyeVqIa5crmJ8oswCnSM0IE8m6qJk0RkRZvdJzRdWgGhWjri0Mk+Bh/6gRZPOpjpfjHhtFv2qAqPH1G3z/KaheH/PsckGx9/iQdAIvRkK02/e9ne82ON8sy05RN8WPgCMm4kk5gdfdlD7ZUUtOqlKgNHmQyggjgd7DfxgFzPI62P9zvHiHyYGHubZ/XJYwWVHy6oP9PIcUxEH01MnoNxKYwFDLD6ujG0+Cb4umB8SacPNnXdjZ8AG+gndTlHdqRXm7KPaAaGqA+HuPlRJysBEnuvr6WeCZOJ6KWjgK+Z0xaR7GQB1obfId7Pc3zhqYXf9G0+hMLVAYJ+eJrfEZauYPtsAAW0qeOiyEDsJkFyv0Qrnee537R/12A0mV1YjWIBSvvYXQJzyUx01FIR9v0hFnT7/0P4L9cluoKxZt4Yl4q57edPQ6XlerhG/1zbkCRDqq/xaG5RSEw3nbhTTbNE8WHj3lkJ9Qd1+YCjr9Ta1xJjJpEdfBVBQ5UwAR3BCuu5Hkmg7WG4q/zDnw6qkPN2s=
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 Dropbox, Inc.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include runtests
3 | include README
4 | include README.md
5 | include requirements.txt
6 | include requirements-dev.txt
7 | recursive-include config *
8 | recursive-include tests *
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/dropbox/hermes)
3 | # Introduction #
4 |
5 | Hermes logs events, generates tasks, and tracks tasks in logical groups.
6 |
7 | # Documentation #
8 |
9 | [dbx-hermes.readthedocs.org](http://dbx-hermes.readthedocs.org/en/latest/)
10 |
11 | # How to Build
12 |
13 | 1. gulp build
14 | 2. python setup.py sdist
15 |
--------------------------------------------------------------------------------
/bin/hermes-notify:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import argparse
4 | import logging
5 | import textwrap
6 |
7 | import hermes
8 | from hermes.util import PluginHelper
9 | from hermes.models import get_db_session, get_db_engine, Session, Quest, Labor
10 | from hermes.settings import settings
11 | from hermes.util import email_message
12 |
13 | from sqlalchemy.exc import OperationalError
14 |
15 | sa_log = logging.getLogger("sqlalchemy.engine.base.Engine")
16 |
17 |
18 | def parse_args():
19 | parser = argparse.ArgumentParser(description="Hermes Web Service")
20 | parser.add_argument("-c", "--config", default="/etc/hermes/server.yaml",
21 | help="Path to config file.")
22 | parser.add_argument(
23 | "-v", "--verbose", action="count", default=0,
24 | help="Increase logging verbosity."
25 | )
26 | parser.add_argument(
27 | "-q", "--quiet", action="count", default=0,
28 | help="Decrease logging verbosity."
29 | )
30 | parser.add_argument(
31 | "-V", "--version", action="version",
32 | version="%%(prog)s %s" % hermes.__version__,
33 | help="Display version information."
34 | )
35 | parser.add_argument(
36 | "-s", "--send-to", type=str, default=None, help="Send all reports to this email address"
37 | )
38 | return parser.parse_args()
39 |
40 |
41 | def find_quest(quests, quest_id):
42 | for quest in quests:
43 | if quest.id == quest_id:
44 | return quest
45 |
46 | return None
47 |
48 |
49 | def main():
50 | args = parse_args()
51 | settings.update_from_config(args.config)
52 |
53 | if args.verbose:
54 | logging.basicConfig(level=logging.DEBUG, format=settings.log_format)
55 | elif args.quiet:
56 | logging.basicConfig(level=logging.ERROR, format=settings.log_format)
57 | else:
58 | logging.basicConfig(level=logging.INFO, format=settings.log_format)
59 |
60 | logging.info(
61 | "Starting Hermes notification system"
62 | )
63 |
64 | # Connect to the database and get the open quests
65 | db_engine = get_db_engine(settings.database)
66 | Session.configure(bind=db_engine)
67 | session = Session()
68 | open_quests = Quest.get_open_quests(session)
69 | open_labors = Labor.get_open_labors(session)
70 |
71 | # get the list of all the hostnames that have open labors
72 | hostnames = set()
73 | for labor in open_labors:
74 | hostnames.add(labor.host.hostname)
75 |
76 | # get the owner information
77 | try:
78 | results = PluginHelper.request_post(
79 | json_body={
80 | "operation": "owners",
81 | "hostnames": list(hostnames)
82 | }
83 | )
84 | owners = results.json()['results']
85 | except Exception as e:
86 | logging.error("Failed to get host owners: " + e.message)
87 |
88 | strongpoc_contacts = None
89 | if settings.strongpoc_server:
90 | # get the contact information
91 | try:
92 | results = PluginHelper.request_get(
93 | path = "/api/pocs/?expand=teams&expand=service_providers&expand=contact_types&service_provider__name=hermes&contact_type__name=email",
94 | server = settings.strongpoc_server
95 | )
96 | strongpoc_results = results.json()
97 | strongpoc_contacts = {}
98 | for result in strongpoc_results:
99 | strongpoc_contacts[result['team']['name']] = result['value']
100 | except Exception as e:
101 | logging.error("Failed to get strongpoc contacts: " + e.message)
102 |
103 | # get the tags for hosts
104 | try:
105 | results = PluginHelper.request_post(
106 | json_body={
107 | "operation": "tags",
108 | "hostnames": list(hostnames)
109 | }
110 | )
111 | tags = results.json()['results']
112 | except Exception as e:
113 | logging.error("Failed to get host tags: " + e.message)
114 |
115 | # map the labors and quests to owners
116 | info = {}
117 | for labor in open_labors:
118 | if labor.for_creator and not labor.quest:
119 | # FIXME: what should we do here? See Issue #145
120 | continue
121 | elif labor.for_creator and labor.quest:
122 | if labor.quest.creator not in info:
123 | info[labor.quest.creator] = {}
124 |
125 | # if this is our first time seeing this owner, create the empty record
126 | elif owners[labor.host.hostname] not in info:
127 | info[owners[labor.host.hostname]] = {}
128 |
129 | owner = (
130 | info[owners[labor.host.hostname]] if labor.for_owner
131 | else info[labor.quest.creator]
132 | )
133 |
134 | quest_id = labor.quest_id if labor.quest else 0
135 | if quest_id not in owner:
136 | owner[quest_id] = []
137 |
138 | owner[quest_id].append(labor.host.hostname)
139 |
140 | # generate and send emails
141 | for owner in info:
142 | plain_msg = generate_plain_mesg(info, open_quests, owner, tags)
143 | html_msg = generate_html_mesg(info, open_quests, owner, tags)
144 |
145 | # send to strongPOC specified email, otherwise send to owner@domainname
146 | if strongpoc_contacts and owner in strongpoc_contacts:
147 | recipient = strongpoc_contacts[owner]
148 | else:
149 | recipient = "{}@{}".format(
150 | owner, settings.domain
151 | )
152 |
153 | # always send email to args.send_to if defined
154 | if args.send_to:
155 | logging.debug('Overriding {} recipient for owner {}'.format(recipient, owner))
156 | recipient = args.send_to
157 | logging.debug('Sending email to {}'.format(recipient))
158 |
159 | email_message(
160 | recipient,
161 | "{}: Open Hermes labors need your attention".format(owner),
162 | plain_msg, html_message=html_msg
163 | )
164 |
165 |
166 | def generate_plain_mesg(info, open_quests, owner, tags):
167 | """Generate the plain text version of the 'open labors' email
168 |
169 | Args:
170 | info: the gathered and sorted open labor information
171 | open_quests: the information on open quests
172 | owner: the owner we care about emailing
173 | tags: the host tags information
174 |
175 | Returns:
176 | a plain text message to be sent to the owner specified
177 | """
178 |
179 | msg = (
180 | "This email is being sent to {} because that is the owner listed\n"
181 | "for the systems with open Hermes labors listed below.\n\n"
182 | "Due dates, if any, are noted with each quest.\n".format(owner)
183 | )
184 | msg += (
185 | "\nTo throw an event manually, you can run the following command "
186 | "on a shell server:"
187 | "\n\n"
188 | "$ hermes event create [event] --host [hostname].\n\n"
189 | "Or you can visit the quests linked below.\n\n".format(
190 | settings.frontend)
191 | )
192 | for quest_id in info[owner]:
193 | quest = find_quest(open_quests, quest_id)
194 | if quest:
195 | msg += (
196 | "==[ QUEST {} ]================================\n"
197 | "CREATOR: {}\n"
198 | ).format(
199 | quest_id, quest.creator
200 | )
201 | if quest.target_time:
202 | msg += "DUE: {}\n".format(quest.target_time)
203 | msg += "DESC: \"{}\"\n".format(textwrap.fill(
204 | quest.description,
205 | width=60, subsequent_indent=""
206 | ))
207 | msg += "LINK: {}/v1/quests/{}\n\n".format(
208 | settings.frontend, quest_id
209 | )
210 | else:
211 | msg += " Labors not associated with a quest:\n\n"
212 |
213 | msg += "Machines with labors:\n"
214 |
215 | for hostname in sorted(info[owner][quest_id]):
216 | if tags[hostname]:
217 | tags_str = "{}".format((", ".join(tags[hostname])))
218 | else:
219 | tags_str = "no services"
220 | msg += " {} ({})\n".format(hostname, tags_str)
221 |
222 | msg += "\n\n"
223 |
224 | return msg
225 |
226 |
227 | def generate_html_mesg(info, open_quests, owner, tags):
228 | """Generate the HTML version of the 'open labors' email
229 |
230 | Args:
231 | info: the gathered and sorted open labor information
232 | open_quests: the information on open quests
233 | owner: the owner we care about emailing
234 | tags: the host tags information
235 |
236 | Returns:
237 | an HTML doc of the message to be sent to the owner specified
238 | """
239 |
240 | msg = '' \
241 | '
'
242 | msg += (
243 | ""
245 | "Hermes Notifications"
246 | "
"
247 | "This email is being sent to {} because that is the owner listed\n"
248 | "for the systems with open Hermes labors listed below.
"
249 | "
Due dates, if any, are noted with each quest.
"
250 | "".format(owner)
251 | )
252 | msg += (
253 | "
To throw an event manually, you can run the following command "
254 | "on a shell server:
"
255 | "
$ hermes event create [event] --host "
256 | "[hostname] "
257 | "
Or you can visit the quests linked below.
".format(
258 | settings.frontend)
259 | )
260 | for quest_id in info[owner]:
261 | quest = find_quest(open_quests, quest_id)
262 | if quest:
263 | msg += (
264 | ""
266 | "
QUEST {} "
267 | "
CREATOR: {}
"
268 | ).format(
269 | quest_id, quest.creator
270 | )
271 | if quest.target_time:
272 | msg += "
DUE: {}
".format(quest.target_time)
273 | msg += "
DESC: \"{}\"
".format(quest.description)
274 | msg += "
LINK: {}/v1/quests/{}
".format(
275 | settings.frontend, quest_id
276 | )
277 | else:
278 | msg += (
279 | "
"
281 | "
Labors not "
282 | "associated with a quest: "
283 | )
284 |
285 | msg += "
Machines with labors:
"
286 |
287 | msg += "
"
288 | for hostname in sorted(info[owner][quest_id]):
289 | if tags[hostname]:
290 | tags_str = "{}".format((", ".join(tags[hostname])))
291 | else:
292 | tags_str = "no services"
293 | msg += "{} ({})\n".format(hostname, tags_str)
294 |
295 | msg += " "
296 |
297 | msg += ""
298 |
299 | return msg
300 |
301 |
302 | if __name__ == "__main__":
303 | main()
304 |
--------------------------------------------------------------------------------
/bin/hermes-server:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import argparse
4 | import logging
5 | import os
6 | import tornado.ioloop
7 | import tornado.httpserver
8 | import tornado.web
9 |
10 | import hermes
11 | from hermes import version
12 | from hermes.app import Application
13 | from hermes.settings import settings
14 | from hermes.plugin import get_hooks
15 | from hermes import models
16 |
17 |
18 | from sqlalchemy.exc import OperationalError
19 |
20 | try:
21 | from raven.contrib.tornado import AsyncSentryClient
22 | raven_installed = True
23 | except ImportError:
24 | raven_installed = False
25 |
26 | sa_log = logging.getLogger("sqlalchemy.engine.base.Engine")
27 |
28 |
29 | def parse_args():
30 | parser = argparse.ArgumentParser(description="Hermes Web Service")
31 | parser.add_argument("-c", "--config", default="/etc/hermes/server.yaml",
32 | help="Path to config file.")
33 | parser.add_argument(
34 | "-v", "--verbose", action="count", default=0,
35 | help="Increase logging verbosity."
36 | )
37 | parser.add_argument(
38 | "-q", "--quiet", action="count", default=0,
39 | help="Decrease logging verbosity."
40 | )
41 | parser.add_argument(
42 | "-V", "--version", action="version",
43 | version="%%(prog)s %s" % hermes.__version__,
44 | help="Display version information."
45 | )
46 | parser.add_argument(
47 | "-p", "--port", type=int, default=None, help="Override port in config."
48 | )
49 | return parser.parse_args()
50 |
51 |
52 | def main():
53 |
54 | args = parse_args()
55 | settings.update_from_config(args.config)
56 |
57 | if args.verbose:
58 | logging.basicConfig(level=logging.DEBUG, format=settings.log_format)
59 | elif args.quiet:
60 | logging.basicConfig(level=logging.ERROR, format=settings.log_format)
61 | else:
62 | logging.basicConfig(level=logging.INFO, format=settings.log_format)
63 |
64 | tornado_settings = {
65 | "static_path": os.path.join(os.path.dirname(hermes.__file__), "static"),
66 | "debug": settings.debug,
67 | "xsrf_cookies": False,
68 | "cookie_secret": settings.secret_key,
69 | }
70 |
71 | # load and register any hooks we have
72 | # hooks = get_hooks([settings.plugin_dir])
73 | # for hook in hooks:
74 | # logging.debug("registering hook {}".format(hook))
75 | # models.register_hook(hook)
76 |
77 | my_settings = {
78 | "db_uri": settings.database,
79 | "db_engine": None,
80 | "db_session": None,
81 | "domain": settings.domain,
82 | "count_events": settings.count_events,
83 | }
84 |
85 | application = Application(my_settings=my_settings, **tornado_settings)
86 |
87 | logging.info("HERMES SERVER v{}".format(version.__version__))
88 |
89 | # If Sentry DSN is set, try to import raven
90 | if settings.sentry_dsn:
91 | if not raven_installed:
92 | logging.warning(
93 | 'Sentry DSN set but raven not installed. Not enabling Sentry.'
94 | )
95 | else:
96 | logging.info(
97 | 'Sentry DSN set and raven installed. Enabling Sentry.'
98 | )
99 | application.sentry_client = AsyncSentryClient(settings.sentry_dsn)
100 | else:
101 | logging.info('Sentry DSN not set. Not enabling Sentry.')
102 |
103 | port = args.port or settings.port
104 |
105 | logging.info(
106 | "Starting application server with %d processes on port %d",
107 | settings.num_processes, port
108 | )
109 |
110 | server = tornado.httpserver.HTTPServer(application)
111 | server.bind(port, address=settings.bind_address)
112 | server.start(settings.num_processes)
113 | try:
114 | tornado.ioloop.IOLoop.instance().start()
115 | except KeyboardInterrupt:
116 | tornado.ioloop.IOLoop.instance().stop()
117 | finally:
118 | print "Bye"
119 |
120 |
121 | if __name__ == "__main__":
122 | main()
123 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hermes",
3 | "version": "0.0.0",
4 | "private": true,
5 | "homepage": "https://github.com/diggyk/hermes",
6 | "authors": [
7 | "Digant Kasundra
"
8 | ],
9 | "ignore": [
10 | "**/.*",
11 | "node_modules",
12 | "bower_components",
13 | "_bc",
14 | "test",
15 | "tests"
16 | ],
17 | "dependencies": {
18 | "angular": "~1.4.5",
19 | "angular-animate": "~1.4.5",
20 | "angular-location-update": "*",
21 | "angular-route": "~1.4.5",
22 | "angular-smooth-scroll": "ngSmoothScroll#~1.7.1",
23 | "bootstrap": "~3.3.5",
24 | "d3": "~3.5.6",
25 | "jquery": "~2.1.4",
26 | "raphael": "raphael.js#~2.1.4",
27 | "angular-bootstrap": "~0.14.0",
28 | "angular-cookies": "~1.4.7"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/config/dev-client.yaml:
--------------------------------------------------------------------------------
1 | # Format for logging output.
2 | # See https://docs.python.org/2/library/logging.html#logrecord-attributes
3 | # Type: str
4 | log_format: "%(asctime)-15s\t%(levelname)s\t%(message)s"
5 |
6 | # Passing debug option down tornado. Useful for development to
7 | # automatically reload code.
8 | # Type: bool
9 | debug: true
10 |
11 | # The location of the Hermes server
12 | hermes_server: "http://localhost:10901"
--------------------------------------------------------------------------------
/config/dev.yaml:
--------------------------------------------------------------------------------
1 | # Format for logging output.
2 | # See https://docs.python.org/2/library/logging.html#logrecord-attributes
3 | # Type: str
4 | log_format: "%(asctime)-15s\t%(levelname)s\t%(message)s"
5 |
6 | # Number of worker processes to fork for receving requests. This option
7 | # is mutually exclusive with debug.
8 | # Type: int
9 | num_processes: 1
10 |
11 | # The port to listen to requests on.
12 | # Type: int
13 | port: 10901
14 |
15 | # The address to bind to. By default it listens on all interfaces.
16 | # Type: string
17 | bind_address: "127.0.0.1"
18 |
19 | # Passing debug option down tornado. Useful for development to
20 | # automatically reload code.
21 | # Type: bool
22 | debug: true
23 |
24 | # The domain name to append to user names if not specified
25 | domain: "dropbox.com"
26 |
27 | # Specifies whether to use XSRF headers/cookies for API calls. Default: true
28 | # Type: bool
29 | api_xsrf_enabled: false
30 |
31 | # Takes a SqlAlchemy URL to the database. More details
32 | # can be found at the following URL:
33 | # http://docs.sqlalchemy.org/en/rel_0_9/core/engines.html#database-urls
34 | #
35 | # Type: str
36 | database: "mysql://localhost:3306/emsdb?user=emsdb&passwd=testpw"
37 |
38 | # The server to use to host queries
39 | query_server: "http://localhost:5353/api/query"
40 |
41 | # Slack integration (optional)
42 | # slack_webhook: "https://hooks.slack.com/services/"
43 | # slack_proxyhost: "proxyserver:port"
44 |
45 | # Email notifications
46 | email_notifications: false
47 | # email_sender_address: "hermes@localhost"
48 |
49 | # Always send email notifications to this comma seperated list
50 | # email_always_copy: "admin@company.com"
51 |
52 | # This is the expiration (in seconds) of auth_tokens used for API calls
53 | # Type: int
54 | auth_token_expiry: 600
55 |
56 | # Sentry DSN if using Sentry to log exceptions.
57 | # sentry_dsn:
58 |
59 | # Additional plugin directory (full path)
60 | # plugin_dir:
61 |
62 | # Specify the org identifier for FullStory integration
63 | # fullstory_id:
64 |
65 | # StrongPOC integration (optional)
66 | # strongpoc_server:
67 |
68 | # Specify the environment - dev is default, set to prod for production
69 | # environment: "dev"
70 |
71 | # if environment is dev, send emails to the following email address instead
72 | # of actual recipients
73 | # dev_email_recipient:
74 |
--------------------------------------------------------------------------------
/db/bootstrap.sql:
--------------------------------------------------------------------------------
1 | # Dump of table event_types
2 | # ------------------------------------------------------------
3 |
4 | LOCK TABLES `event_types` WRITE;
5 |
6 | INSERT INTO `event_types` (`id`, `category`, `state`, `description`)
7 | VALUES
8 | (1,'system-reboot','required','This system requires a reboot.'),
9 | (2,'system-reboot','completed','This system rebooted.'),
10 | (3,'system-maintenance','required','This system requires maintenance.'),
11 | (4,'system-maintenance','ready','This system is ready for maintenance.'),
12 | (5,'system-maintenance','completed','System maintenance completed.');
13 |
14 | UNLOCK TABLES;
15 |
16 | # Dump of table fates
17 | # ------------------------------------------------------------
18 |
19 | LOCK TABLES `fates` WRITE;
20 |
21 | INSERT INTO `fates` (`id`, `creation_type_id`, `completion_type_id`, `follows_id`, `for_creator`, `for_owner`, `description`)
22 | VALUES
23 | (1, 1, 2, NULL, 0, 1, 'A system that needs a reboot can be cleared by rebooting the machine.'),
24 | (2, 3, 4, NULL, 0, 1, 'System must be released using \"cloudbox release\" so maintenance can be carried out.'),
25 | (3, 4, 5, 2, 1, 0, 'Maintenance must be performed on a system that is prepped.'),
26 | (4, 1, 4, NULL, 0, 1, 'A system that needs a reboot can also just be released.');
27 |
28 |
29 | UNLOCK TABLES;
30 |
--------------------------------------------------------------------------------
/db/truncate_tables.sql:
--------------------------------------------------------------------------------
1 | SET FOREIGN_KEY_CHECKS = 0;
2 | TRUNCATE TABLE quests;
3 | TRUNCATE TABLE labors;
4 | TRUNCATE TABLE hosts;
5 | TRUNCATE TABLE events;
6 | SET FOREIGN_KEY_CHECKS = 1;
7 |
--------------------------------------------------------------------------------
/db/update_to_046.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `fates` ADD `for_owner` INT(1) NOT NULL DEFAULT 1 AFTER `follows_id`;
2 | ALTER TABLE `fates` ADD `for_creator` INT(1) NOT NULL DEFAULT 0 AFTER `follows_id`;
3 | ALTER TABLE `labors` ADD `for_owner` INT(1) NOT NULL DEFAULT 1 AFTER `host_id`;
4 | ALTER TABLE `labors` ADD `for_creator` INT(1) NOT NULL DEFAULT 0 AFTER `host_id`;
5 |
6 | UPDATE `fates` SET `for_owner`=0 where `id`=2;
7 | UPDATE `fates` SET `for_creator`=1 where `id`=2;
8 |
9 | UPDATE `labors` SET `for_owner`=1, `for_creator`=0 where `starting_labor_id` IS NULL;
10 | UPDATE `labors` SET `for_owner`=0, `for_creator`=1 where `starting_labor_id` IS NOT NULL;
11 |
--------------------------------------------------------------------------------
/db/update_to_05.sql:
--------------------------------------------------------------------------------
1 | TRUNCATE TABLE `fates`;
2 | ALTER TABLE `fates` DROP FOREIGN KEY `fates_ibfk_2`;
3 | ALTER TABLE `fates` DROP `completion_type_id`;
4 |
5 | INSERT INTO fates
6 | VALUES
7 | (1,1,NULL, 0, 1, 'Reboot or release the system.');
8 |
9 | INSERT INTO fates
10 | VALUES
11 | (2,2,1, 0, 1, 'A reboot finishes labors.');
12 |
13 | INSERT INTO fates
14 | VALUES
15 | (3,3,NULL, 0, 1, 'Release or acknowledge downtime');
16 |
17 | INSERT INTO fates
18 | VALUES
19 | (4,4,3, 0, 1, 'Perform maintenance');
20 |
21 | INSERT INTO fates
22 | VALUES
23 | (5,5,4, 1, 0, 'Maintenance completed');
24 |
25 | ALTER TABLE `labors` ADD `fate_id` INT(11) NOT NULL DEFAULT 0 AFTER `starting_labor_id`;
26 | ALTER TABLE `labors` ADD INDEX `ix_labors_fate_id` (`fate_id`);
27 |
28 | UPDATE `labors` SET `fate_id`=1;
29 | ALTER TABLE `labors` ADD FOREIGN KEY (`fate_id`) REFERENCES `fates` (`id`);
30 |
31 | UPDATE `labors` l
32 | SET `fate_id` = (
33 | SELECT f.id
34 | FROM `events` e, `event_types` et, `fates` f
35 | WHERE l.creation_event_id = e.id AND e.event_type_id = et.id AND
36 | f.creation_type_id = et.id
37 | );
38 |
39 | INSERT INTO fates
40 | VALUES
41 | (6,4,1, 0, 1, 'A release finishes labors');
42 |
43 |
--------------------------------------------------------------------------------
/db/update_to_051.sql:
--------------------------------------------------------------------------------
1 | SET foreign_key_checks=0;
2 | DROP TABLE IF EXISTS event_types;
3 |
4 | CREATE TABLE event_types (
5 | id int(11) NOT NULL AUTO_INCREMENT,
6 | category varchar(64) COLLATE utf8_unicode_ci NOT NULL,
7 | state varchar(32) COLLATE utf8_unicode_ci NOT NULL,
8 | description varchar(1024) COLLATE utf8_unicode_ci DEFAULT NULL,
9 | PRIMARY KEY (id),
10 | UNIQUE KEY _category_state_uc (category,state),
11 | KEY event_type_idx (id,category,state)
12 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
13 |
14 | LOCK TABLES event_types WRITE;
15 | /*!40000 ALTER TABLE event_types DISABLE KEYS */;
16 |
17 | INSERT INTO event_types (id, category, state, description)
18 | VALUES
19 | (1,'system-reboot','required','System requires a reboot'),
20 | (2,'system-reboot','completed','System rebooted'),
21 | (3,'system-maintenance','required','System requires maintenance'),
22 | (4,'system-maintenance','ready','System ready for maintenance'),
23 | (5,'system-maintenance','completed','System maintenance completed'),
24 | (6,'system-maintenance','acknowledge','Acknowledge system maintenance'),
25 | (7,'system-maintenance','cancel','Cancel system maintenance');
26 |
27 | /*!40000 ALTER TABLE event_types ENABLE KEYS */;
28 | UNLOCK TABLES;
29 |
30 |
31 | # Dump of table fates
32 | # ------------------------------------------------------------
33 |
34 | DROP TABLE IF EXISTS fates;
35 |
36 | CREATE TABLE fates (
37 | id int(11) NOT NULL AUTO_INCREMENT,
38 | creation_type_id int(11) NOT NULL,
39 | follows_id int(11) DEFAULT NULL,
40 | for_creator int(1) NOT NULL DEFAULT '0',
41 | for_owner int(1) NOT NULL DEFAULT '1',
42 | description varchar(2048) COLLATE utf8_unicode_ci DEFAULT NULL,
43 | PRIMARY KEY (id),
44 | UNIQUE KEY _creation_completion_uc (creation_type_id,follows_id),
45 | KEY ix_fates_creation_type_id (creation_type_id),
46 | KEY fate_idx (id,creation_type_id,follows_id),
47 | KEY ix_fates_follows_id (follows_id),
48 | CONSTRAINT fates_ibfk_1 FOREIGN KEY (creation_type_id) REFERENCES event_types (id),
49 | CONSTRAINT fates_ibfk_3 FOREIGN KEY (follows_id) REFERENCES fates (id)
50 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
51 |
52 | LOCK TABLES fates WRITE;
53 | /*!40000 ALTER TABLE fates DISABLE KEYS */;
54 |
55 | INSERT INTO fates (id, creation_type_id, follows_id, for_creator, for_owner, description)
56 | VALUES
57 | (1,1,NULL,0,1,'Reboot or release the system'),
58 | (2,2,1,0,1,'System rebooted'),
59 | (3,3,NULL,0,1,'Release or acknowledge downtime'),
60 | (4,4,3,1,0,'Perform maintenance'),
61 | (5,5,4,1,0,'Maintenance completed'),
62 | (6,6,3,1,0,'Perform online maintenance'),
63 | (7,5,6,1,0,'Maintenance completed'),
64 | (8,7,3,1,0,'Maintenance cancelled'),
65 | (9,7,4,1,0,'Maintenance cancelled'),
66 | (10,7,6,1,0,'Maintenance cancelled');
67 |
68 | /*!40000 ALTER TABLE fates ENABLE KEYS */;
69 | UNLOCK TABLES;
70 | SET foreign_key_checks=1;
--------------------------------------------------------------------------------
/db/update_to_0511.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `labors` ADD `closing_fate_id` INT(11) DEFAULT NULL AFTER `fate_id`;
2 | ALTER TABLE `labors` ADD INDEX `ix_labors_closing_fate_id` (`closing_fate_id`);
3 |
4 | UPDATE `labors` SET `closing_fate_id`=4 WHERE `fate_id`=3;
5 | UPDATE `labors` SET `closing_fate_id`=5 WHERE `fate_id`=4;
6 | ALTER TABLE `labors` ADD FOREIGN KEY (`closing_fate_id`) REFERENCES `fates` (`id`);
7 |
--------------------------------------------------------------------------------
/db/update_to_06.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `event_types` ADD `restricted` INT(1) NOT NULL DEFAULT '0' AFTER `description`;
2 |
--------------------------------------------------------------------------------
/db/update_to_0726.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `events` MODIFY `note` text;
2 |
--------------------------------------------------------------------------------
/db/update_to_0_5_15.sql:
--------------------------------------------------------------------------------
1 | drop index _creation_completion_uc on fates;
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # User-friendly check for sphinx-build
11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
13 | endif
14 |
15 | # Internal variables.
16 | PAPEROPT_a4 = -D latex_paper_size=a4
17 | PAPEROPT_letter = -D latex_paper_size=letter
18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
19 | # the i18n builder cannot share the environment and doctrees with the others
20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
21 |
22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
23 |
24 | help:
25 | @echo "Please use \`make ' where is one of"
26 | @echo " html to make standalone HTML files"
27 | @echo " dirhtml to make HTML files named index.html in directories"
28 | @echo " singlehtml to make a single large HTML file"
29 | @echo " pickle to make pickle files"
30 | @echo " json to make JSON files"
31 | @echo " htmlhelp to make HTML files and a HTML help project"
32 | @echo " qthelp to make HTML files and a qthelp project"
33 | @echo " devhelp to make HTML files and a Devhelp project"
34 | @echo " epub to make an epub"
35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
36 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
38 | @echo " text to make text files"
39 | @echo " man to make manual pages"
40 | @echo " texinfo to make Texinfo files"
41 | @echo " info to make Texinfo files and run them through makeinfo"
42 | @echo " gettext to make PO message catalogs"
43 | @echo " changes to make an overview of all changed/added/deprecated items"
44 | @echo " xml to make Docutils-native XML files"
45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
46 | @echo " linkcheck to check all external links for integrity"
47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
48 |
49 | clean:
50 | rm -rf $(BUILDDIR)/*
51 |
52 | html:
53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
54 | @echo
55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
56 |
57 | dirhtml:
58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
59 | @echo
60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
61 |
62 | singlehtml:
63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
64 | @echo
65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
66 |
67 | pickle:
68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
69 | @echo
70 | @echo "Build finished; now you can process the pickle files."
71 |
72 | json:
73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
74 | @echo
75 | @echo "Build finished; now you can process the JSON files."
76 |
77 | htmlhelp:
78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
79 | @echo
80 | @echo "Build finished; now you can run HTML Help Workshop with the" \
81 | ".hhp project file in $(BUILDDIR)/htmlhelp."
82 |
83 | qthelp:
84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
85 | @echo
86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/NetworkSourceofTruth.qhcp"
89 | @echo "To view the help file:"
90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/NetworkSourceofTruth.qhc"
91 |
92 | devhelp:
93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
94 | @echo
95 | @echo "Build finished."
96 | @echo "To view the help file:"
97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/NetworkSourceofTruth"
98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/NetworkSourceofTruth"
99 | @echo "# devhelp"
100 |
101 | epub:
102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
103 | @echo
104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
105 |
106 | latex:
107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
108 | @echo
109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
111 | "(use \`make latexpdf' here to do that automatically)."
112 |
113 | latexpdf:
114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
115 | @echo "Running LaTeX files through pdflatex..."
116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
118 |
119 | latexpdfja:
120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
121 | @echo "Running LaTeX files through platex and dvipdfmx..."
122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
124 |
125 | text:
126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
127 | @echo
128 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
129 |
130 | man:
131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
132 | @echo
133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
134 |
135 | texinfo:
136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
137 | @echo
138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
139 | @echo "Run \`make' in that directory to run these through makeinfo" \
140 | "(use \`make info' here to do that automatically)."
141 |
142 | info:
143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
144 | @echo "Running Texinfo files through makeinfo..."
145 | make -C $(BUILDDIR)/texinfo info
146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
147 |
148 | gettext:
149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
150 | @echo
151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
152 |
153 | changes:
154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
155 | @echo
156 | @echo "The overview file is in $(BUILDDIR)/changes."
157 |
158 | linkcheck:
159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
160 | @echo
161 | @echo "Link check complete; look for any errors in the above output " \
162 | "or in $(BUILDDIR)/linkcheck/output.txt."
163 |
164 | doctest:
165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
166 | @echo "Testing of doctests in the sources finished, look at the " \
167 | "results in $(BUILDDIR)/doctest/output.txt."
168 |
169 | xml:
170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
171 | @echo
172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
173 |
174 | pseudoxml:
175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
176 | @echo
177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
178 |
--------------------------------------------------------------------------------
/docs/api-ref.rst:
--------------------------------------------------------------------------------
1 | .. _api-ref:
2 |
3 | =============
4 | API Reference
5 | =============
6 |
7 | .. autotornado:: hermes.app:Application()
8 | :endpoints:
9 |
--------------------------------------------------------------------------------
/docs/api.rst:
--------------------------------------------------------------------------------
1 | API Documentation
2 | *****************
3 |
4 | Hermes is designed as an API first so anything possible in the Web UI
5 | or command line tools would be available here.
6 |
7 | Authentication
8 | --------------
9 |
10 | Authentication is still in the works. Right now, Hermes API is expected to sit behind some kind of authenticating proxy.
11 |
12 | Requests
13 | --------
14 |
15 | In addition to the authentication header above all ``POST``/``PUT`` requests
16 | will be sent as json rather than form data and should include the header ``Content-Type: application/json``
17 |
18 |
19 | Responses
20 | ---------
21 | All responses will be in ``JSON`` format along with the header
22 | ``Content-Type: application/json`` set.
23 |
24 | The ``JSON`` payload will be in one of two potential structures and will always contain a ``status`` field to distinguish between them. If the ``status`` field
25 | has a value of ``"ok"`` or ``"created"``, then the request (or creation, respectively) was successful and the response will
26 | be available the remaining fields.
27 |
28 | .. sourcecode:: javascript
29 |
30 | {
31 | "status": "ok",
32 | "id": 1,
33 | ...
34 | }
35 |
36 | If the ``status`` field has a value of ``"error"`` then the response failed
37 | in some way. You will have access to the error from the ``error`` field which
38 | will contain an error ``code`` and ``message``.
39 |
40 | .. sourcecode:: javascript
41 |
42 | {
43 | "status": "error",
44 | "error": {
45 | "code": 404,
46 | "message": "Resource not found."
47 | }
48 | }
49 |
50 | Pagination
51 | ----------
52 |
53 | Most, if not all, responses that return a list of resources will support pagination. If the
54 | ``data`` object on the response has a ``total`` attribute then the endpoint supports pagination.
55 | When making a request against this endpoint ``limit`` and ``offset`` query parameters are
56 | supported.
57 |
58 | An example response for querying the ``sites`` endpoint might look like:
59 |
60 | .. sourcecode:: javascript
61 |
62 | {
63 | "status": "ok",
64 | "hosts": [
65 | {
66 | "id": 1
67 | "hostname": "example",
68 | "href": "/api/v1/hostname/example",
69 | }
70 | ],
71 | "limit": 10,
72 | "offset": 0,
73 | "total": 1
74 | }
75 |
76 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Network Source of Truth documentation build configuration file, created by
4 | # sphinx-quickstart on Fri Dec 26 11:33:34 2014.
5 | #
6 | # This file is execfile()d with the current directory set to its
7 | # containing dir.
8 | #
9 | # Note that not all possible configuration values are present in this
10 | # autogenerated file.
11 | #
12 | # All configuration values have a default; values that are commented out
13 | # serve to show the default.
14 |
15 | import sys
16 | import os
17 | import sphinx_rtd_theme
18 |
19 |
20 | # If extensions (or modules to document with autodoc) are in another directory,
21 | # add these directories to sys.path here. If the directory is relative to the
22 | # documentation root, use os.path.abspath to make it absolute, like shown here.
23 | sys.path.insert(0, os.path.abspath('..'))
24 |
25 | # -- General configuration ------------------------------------------------
26 |
27 | # If your documentation needs a minimal Sphinx version, state it here.
28 | #needs_sphinx = '1.0'
29 |
30 | # Add any Sphinx extension module names here, as strings. They can be
31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
32 | # ones.
33 | extensions = [
34 | "sphinxcontrib.httpdomain",
35 | "sphinxcontrib.autohttp.tornado",
36 | ]
37 |
38 | # Add any paths that contain templates here, relative to this directory.
39 | templates_path = ['_templates']
40 |
41 | # The suffix of source filenames.
42 | source_suffix = '.rst'
43 |
44 | # The encoding of source files.
45 | #source_encoding = 'utf-8-sig'
46 |
47 | # The master toctree document.
48 | master_doc = 'index'
49 |
50 | # General information about the project.
51 | project = u'Hermes'
52 | copyright = u'2015, Dropbox, Inc.'
53 |
54 | # The version info for the project you're documenting, acts as replacement for
55 | # |version| and |release|, also used in various other places throughout the
56 | # built documents.
57 | #
58 | # The short X.Y version.
59 | def get_version():
60 | from hermes import __version__
61 | return __version__
62 |
63 | version = get_version()
64 | # The full version, including alpha/beta/rc tags.
65 | release = version
66 |
67 | # The language for content autogenerated by Sphinx. Refer to documentation
68 | # for a list of supported languages.
69 | #language = None
70 |
71 | # There are two options for replacing |today|: either, you set today to some
72 | # non-false value, then it is used:
73 | #today = ''
74 | # Else, today_fmt is used as the format for a strftime call.
75 | #today_fmt = '%B %d, %Y'
76 |
77 | # List of patterns, relative to source directory, that match files and
78 | # directories to ignore when looking for source files.
79 | exclude_patterns = ['_build']
80 |
81 | # The reST default role (used for this markup: `text`) to use for all
82 | # documents.
83 | #default_role = None
84 |
85 | # If true, '()' will be appended to :func: etc. cross-reference text.
86 | #add_function_parentheses = True
87 |
88 | # If true, the current module name will be prepended to all description
89 | # unit titles (such as .. function::).
90 | #add_module_names = True
91 |
92 | # If true, sectionauthor and moduleauthor directives will be shown in the
93 | # output. They are ignored by default.
94 | #show_authors = False
95 |
96 | # The name of the Pygments (syntax highlighting) style to use.
97 | pygments_style = 'sphinx'
98 |
99 | # A list of ignored prefixes for module index sorting.
100 | #modindex_common_prefix = []
101 |
102 | # If true, keep warnings as "system message" paragraphs in the built documents.
103 | #keep_warnings = False
104 |
105 |
106 | # -- Options for HTML output ----------------------------------------------
107 |
108 | html_theme = "sphinx_rtd_theme"
109 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
110 |
111 | # The theme to use for HTML and HTML Help pages. See the documentation for
112 | # a list of builtin themes.
113 | #html_theme = 'default'
114 |
115 | # Theme options are theme-specific and customize the look and feel of a theme
116 | # further. For a list of options available for each theme, see the
117 | # documentation.
118 | #html_theme_options = {}
119 |
120 | # Add any paths that contain custom themes here, relative to this directory.
121 | #html_theme_path = []
122 |
123 | # The name for this set of Sphinx documents. If None, it defaults to
124 | # " v documentation".
125 | #html_title = None
126 |
127 | # A shorter title for the navigation bar. Default is the same as html_title.
128 | #html_short_title = None
129 |
130 | # The name of an image file (relative to this directory) to place at the top
131 | # of the sidebar.
132 | #html_logo = None
133 |
134 | # The name of an image file (within the static path) to use as favicon of the
135 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
136 | # pixels large.
137 | #html_favicon = None
138 |
139 | # Add any paths that contain custom static files (such as style sheets) here,
140 | # relative to this directory. They are copied after the builtin static files,
141 | # so a file named "default.css" will overwrite the builtin "default.css".
142 | html_static_path = ['_static']
143 |
144 | # Add any extra paths that contain custom files (such as robots.txt or
145 | # .htaccess) here, relative to this directory. These files are copied
146 | # directly to the root of the documentation.
147 | #html_extra_path = []
148 |
149 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
150 | # using the given strftime format.
151 | #html_last_updated_fmt = '%b %d, %Y'
152 |
153 | # If true, SmartyPants will be used to convert quotes and dashes to
154 | # typographically correct entities.
155 | #html_use_smartypants = True
156 |
157 | # Custom sidebar templates, maps document names to template names.
158 | #html_sidebars = {}
159 |
160 | # Additional templates that should be rendered to pages, maps page names to
161 | # template names.
162 | #html_additional_pages = {}
163 |
164 | # If false, no module index is generated.
165 | #html_domain_indices = True
166 |
167 | # If false, no index is generated.
168 | #html_use_index = True
169 |
170 | # If true, the index is split into individual pages for each letter.
171 | #html_split_index = False
172 |
173 | # If true, links to the reST sources are added to the pages.
174 | #html_show_sourcelink = True
175 |
176 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
177 | #html_show_sphinx = True
178 |
179 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
180 | #html_show_copyright = True
181 |
182 | # If true, an OpenSearch description file will be output, and all pages will
183 | # contain a tag referring to it. The value of this option must be the
184 | # base URL from which the finished HTML is served.
185 | #html_use_opensearch = ''
186 |
187 | # This is the file name suffix for HTML files (e.g. ".xhtml").
188 | #html_file_suffix = None
189 |
190 | # Output file base name for HTML help builder.
191 | htmlhelp_basename = 'Hermesdoc'
192 |
193 |
194 | # -- Options for LaTeX output ---------------------------------------------
195 |
196 | latex_elements = {
197 | # The paper size ('letterpaper' or 'a4paper').
198 | #'papersize': 'letterpaper',
199 |
200 | # The font size ('10pt', '11pt' or '12pt').
201 | #'pointsize': '10pt',
202 |
203 | # Additional stuff for the LaTeX preamble.
204 | #'preamble': '',
205 | }
206 |
207 | # Grouping the document tree into LaTeX files. List of tuples
208 | # (source start file, target name, title,
209 | # author, documentclass [howto, manual, or own class]).
210 | latex_documents = [
211 | ('index', 'Hermes.tex', u'Hermes Documentation',
212 | u'Digant C Kasundra', 'manual'),
213 | ]
214 |
215 | # The name of an image file (relative to this directory) to place at the top of
216 | # the title page.
217 | #latex_logo = None
218 |
219 | # For "manual" documents, if this is true, then toplevel headings are parts,
220 | # not chapters.
221 | #latex_use_parts = False
222 |
223 | # If true, show page references after internal links.
224 | #latex_show_pagerefs = False
225 |
226 | # If true, show URL addresses after external links.
227 | #latex_show_urls = False
228 |
229 | # Documents to append as an appendix to all manuals.
230 | #latex_appendices = []
231 |
232 | # If false, no module index is generated.
233 | #latex_domain_indices = True
234 |
235 |
236 | # -- Options for manual page output ---------------------------------------
237 |
238 | # One entry per manual page. List of tuples
239 | # (source start file, name, description, authors, manual section).
240 | man_pages = [
241 | ('index', 'hermes', u'Hermes Documentation',
242 | [u'Digant C Kasundra'], 1)
243 | ]
244 |
245 | # If true, show URL addresses after external links.
246 | #man_show_urls = False
247 |
248 |
249 | # -- Options for Texinfo output -------------------------------------------
250 |
251 | # Grouping the document tree into Texinfo files. List of tuples
252 | # (source start file, target name, title, author,
253 | # dir menu entry, description, category)
254 | texinfo_documents = [
255 | ('index', 'Hermes', u'Hermes Documentation',
256 | u'Digant C Kasundra', 'Hermes', 'One line description of project.',
257 | 'Miscellaneous'),
258 | ]
259 |
260 | # Documents to append as an appendix to all manuals.
261 | #texinfo_appendices = []
262 |
263 | # If false, no module index is generated.
264 | #texinfo_domain_indices = True
265 |
266 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
267 | #texinfo_show_urls = 'footnote'
268 |
269 | # If true, do not generate a @detailmenu in the "Top" node's menu.
270 | #texinfo_no_detailmenu = False
271 |
--------------------------------------------------------------------------------
/docs/config.rst:
--------------------------------------------------------------------------------
1 | .. _configuration:
2 |
3 | =============
4 | Configuration
5 | =============
6 |
7 | .. literalinclude:: ../config/dev.yaml
8 | :language: yaml
9 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Hermes
2 | ===================================================
3 |
4 | Contents:
5 |
6 | .. toctree::
7 | :maxdepth: 2
8 |
9 | intro
10 | config
11 | api
12 | api-ref
13 |
--------------------------------------------------------------------------------
/docs/intro.rst:
--------------------------------------------------------------------------------
1 | Introduction
2 | ============
3 |
4 | Hermes logs events, generates tasks, and tracks tasks in logical groups.
5 |
6 | Terminology
7 | ===========
8 |
9 | Rather than mimic the overloaded and overused terminology typically used, and in keeping with the Dropbox principal of "cupcake," Hermes adopts a more interesting language.
10 |
11 | Events and Event Types
12 | ----------------------
13 |
14 | Events double as journal entries, logging system activities like server restarts, and requests for action, such as a need to restart or turn off a server.
15 |
16 | As journal entries, events provide an audit trail and can potentially be used to track a range of activities. As request entries, events can initialize labors and subsequent events would close these labors.
17 |
18 | Each event must be of a predefined event type. An event type consists of a category and state, the combination of which provides meaningful grouping and definition:
19 | ::
20 | ID CATEGORY STATE
21 | [1] system-reboot required
22 | [2] system-reboot completed
23 | [3] system-maintenance required
24 | [4] system-maintenance ready
25 | [5] system-maintenance completed
26 |
27 |
28 | Event types are often written simply as ``category-state``, such as ``system-reboot-required``.
29 |
30 | An individual event entry consists of the event type, the host, and the time of occurrence.
31 |
32 | Labors
33 | ------
34 |
35 | Labors represent tasks that need to be performed or outstanding issues that need to be addressed for a host. All labors are created and closed as the result of events.
36 |
37 | Labors are usually referred to by the event which triggered its creation, so a ``system-reboot-required`` event creates a ``system-reboot-required`` labor.
38 |
39 | Fates
40 | -----
41 | Basics
42 | ``````
43 | The fates define how labors are created and completed. A typical fate will specify which event type will result in the creation of a labor for the host, and which event type will close labors for a host.
44 | ::
45 | [1] system-reboot-required => system-reboot-completed
46 |
47 |
48 | Chained Fates
49 | `````````````
50 | An ``intermediate`` flag in the definition of a fate indicates if the fate only applies to existing labors. This allows fates to be chained together to essentially create a workflow engine.
51 |
52 | For example:
53 | ::
54 | [1] system-maintenance-required => system-maintenance-ready
55 | [2] system-maintenance-ready => system-maintenance-completed
56 |
57 |
58 | (with the second fate being flagged as an intermediate) would essentially mean:
59 | ::
60 | system-maintenance-required => system-maintenance-ready => system-maintenance-completed
61 |
62 | In this example, an event of type ``system-maintenance-ready`` only creates a labor if an existing labor created by an event of type ``system-maintenance-required`` was present.
63 |
64 | Choose Your Own Adventure
65 | `````````````````````````
66 |
67 | Fates can allow multiple ways to resolve a labor.
68 | ::
69 | [1] puppet-restart-required => puppet-restart-completed
70 | [2] puppet-restart-required => system-restart-completed
71 |
72 | In this example, a labor created by the event ``puppet-restart-required`` can be completed by either a ``puppet-restart-completed`` event, or a ``system-restart-completed`` event.
73 |
74 | Quests
75 | ------
76 |
77 | Quests are collections of labors, making tracking and reporting of progress much easier.
78 |
79 | For example, when a security fix is released that requires all web servers to be restarted, a quest can be created with a ``system-restart-required`` labor for all the hosts.
80 |
81 | Quests will eventually contain information to outside references, such as Jira tickets.
82 |
83 | Status
84 | ======
85 |
86 | Development can be tracked at GitHub_ and Travis_CI_
87 |
88 | .. _GitHub: https://github.com/dropbox/hermes
89 | .. _Travis_CI: https://travis-ci.org/dropbox/hermes
90 |
91 | TODOS
92 | =====
93 |
94 | Deletion Support
95 | ----------------
96 |
97 | Currently, nothing can be deleted through the API or client. It would be nice to be able to delete event-types and
98 | fates.
99 |
--------------------------------------------------------------------------------
/examples/ex1/create-quest:
--------------------------------------------------------------------------------
1 | cat examples/ex1/servers.list | bin/hermes -c config/dev-client.yaml -v quest create system-reboot required -d "The quest of ritual healing."
2 |
--------------------------------------------------------------------------------
/examples/ex1/reboot-complete:
--------------------------------------------------------------------------------
1 | for host in $( cat examples/ex1/servers.list | tail -n 20 ); do bin/hermes -c config/dev-client.yaml event create $host system-reboot completed; done
2 |
--------------------------------------------------------------------------------
/examples/ex1/reboot-complete-remaining:
--------------------------------------------------------------------------------
1 | for host in $( cat examples/ex1/servers.list | head -n 10 ); do bin/hermes -c config/dev-client.yaml event create $host system-reboot completed; done
2 |
--------------------------------------------------------------------------------
/examples/ex1/servers.list:
--------------------------------------------------------------------------------
1 | server-10871
2 | server-12178
3 | server-13406
4 | server-13533
5 | server-14488
6 | server-18994
7 | server-19287
8 | server-19753
9 | server-20998
10 | server-21523
11 | server-22306
12 | server-22565
13 | server-22772
14 | server-23445
15 | server-25320
16 | server-25852
17 | server-27560
18 | server-27857
19 | server-29767
20 | server-31114
21 | server-31462
22 | server-3186
23 | server-31902
24 | server-32066
25 | server-3527
26 | server-4393
27 | server-5957
28 | server-6985
29 | server-7121
30 | server-8090
31 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Build tasks are broken up as either top-level tasks, for example `build`,
3 | * or sub-tasks, namespaced with a top-level task name and a color as a prefix,
4 | * for example, `build:js`
5 | *
6 | * You'll likely want to have gulp installed globally if you're using it regularly
7 | * though you'll be able to run it fron `node_modules/.bin/gulp` if you don't
8 | * use it often.
9 | *
10 | * Top Level Tasks
11 | * ---------------
12 | * gulp clean - Remove built assets
13 | * gulp build - Build all static assets for distribution
14 | * gulp lint - Lint JavaScript and CSS files
15 | * gulp bower - Update local cache for web dependencies
16 | */
17 |
18 | var gulp = require('gulp');
19 |
20 | // Plugin Imports
21 | var jshint = require('gulp-jshint');
22 | var concat = require('gulp-concat');
23 | var ngAnnotate = require('gulp-ng-annotate');
24 | var uglify = require('gulp-uglify');
25 | var rename = require('gulp-rename');
26 | var minifyCss = require('gulp-minify-css');
27 | var csslint = require('gulp-csslint');
28 | var mainBowerFiles = require('main-bower-files');
29 | var bower = require('gulp-bower');
30 | var sort = require('gulp-sort');
31 | var del = require('del');
32 | var watch = require('gulp-watch');
33 | var less = require('gulp-less');
34 | var path = require('path');
35 |
36 | var SRC_ROOT = './hermes/webapp/src/';
37 | var BUILD_DEST = './hermes/webapp/build/';
38 | var VENDOR_ROOT = "./_bc/";
39 |
40 | var JS_MAIN_SRC = SRC_ROOT + 'js/hermesApp.js';
41 | var JS_SRC = SRC_ROOT + 'js/**/*.js';
42 | var STYLE_SRC = SRC_ROOT + 'css/**/*.less';
43 | var IMAGE_SRC = SRC_ROOT + 'img/**';
44 | var HTML_SRC = SRC_ROOT + "**/*.html";
45 | var BS_FONT_SRC = VENDOR_ROOT + "bootstrap/dist/fonts/*.woff2";
46 |
47 |
48 | /**
49 | * Task to lint JavaScript files.
50 | */
51 | gulp.task('lint:js', function() {
52 | return gulp.src([JS_MAIN_SRC, JS_SRC])
53 | .pipe(jshint())
54 | .pipe(jshint.reporter('jshint-stylish'));
55 | });
56 |
57 |
58 | /**
59 | * Task to lint CSS files.
60 | */
61 | gulp.task('lint:style', function() {
62 | return gulp.src(STYLE_SRC)
63 | .pipe(csslint())
64 | .pipe(csslint.reporter());
65 | });
66 |
67 |
68 | /**
69 | * Top level Task to run all lint tasks.
70 | */
71 | gulp.task('lint', ['lint:js', 'lint:style']);
72 |
73 |
74 | /**
75 | * Updates the local cache of bower dependencies
76 | */
77 | gulp.task('bower', function() {
78 | return bower({ cmd: 'update'});
79 | });
80 |
81 |
82 | /**
83 | * Task to build JavaScript files.
84 | */
85 | gulp.task('build:js', function() {
86 | return gulp.src([JS_MAIN_SRC, JS_SRC])
87 | .pipe(ngAnnotate())
88 | //.pipe(sort())
89 | .pipe(concat('app.js'))
90 | .pipe(gulp.dest((BUILD_DEST + 'js')))
91 | .pipe(uglify())
92 | .pipe(rename('app.min.js'))
93 | .pipe(gulp.dest((BUILD_DEST + 'js')));
94 | });
95 |
96 | /**
97 | * Task to build our HTML files
98 | */
99 | gulp.task('build:html', function() {
100 | return gulp.src(HTML_SRC)
101 | .pipe(gulp.dest((BUILD_DEST)))
102 | });
103 |
104 |
105 | /**
106 | * Task to build CSS files.
107 | */
108 | gulp.task('build:style', function() {
109 | return gulp.src(STYLE_SRC)
110 | .pipe(less({
111 | paths: [ path.join(__dirname, 'less', 'includes') ]
112 | }))
113 | .pipe(sort())
114 | .pipe(concat('hermes.css'))
115 | .pipe(gulp.dest((BUILD_DEST + 'css')))
116 | .pipe(minifyCss())
117 | .pipe(rename('hermes.min.css'))
118 | .pipe(gulp.dest((BUILD_DEST + 'css')));
119 | });
120 |
121 | /**
122 | * Task to process less files for bootstrap
123 | */
124 | gulp.task('build:bsless', function() {
125 | return gulp.src('_bc/bootstrap/less/bootstrap.less')
126 | .pipe(less({
127 | paths: [ path.join(__dirname, 'less', 'includes') ]
128 | }))
129 | .pipe(gulp.dest(BUILD_DEST + 'css'));
130 | });
131 |
132 |
133 | /**
134 | * Task to "build" images. While we're not doing anything interesting
135 | * now this opens up the option for building sprites if needed. This
136 | * also keeps our src separate from our build where we'll do things like
137 | * hash built files eventually.
138 | */
139 | gulp.task('build:images', function() {
140 | return gulp.src(IMAGE_SRC)
141 | .pipe(gulp.dest((BUILD_DEST + 'img')))
142 | });
143 |
144 | gulp.task('build:fonts', function() {
145 | return gulp.src(BS_FONT_SRC)
146 | .pipe(gulp.dest((BUILD_DEST + 'fonts')))
147 | });
148 |
149 | /**
150 | * Uses bower to install the "main" files into our build. In most cases
151 | * the "main" files are manually specified in the `overrides` section
152 | * of bower.json
153 | */
154 | gulp.task('build:3rdparty', ['bower'], function() {
155 | return gulp.src(mainBowerFiles(), {base: '_bc'})
156 | .pipe(gulp.dest(BUILD_DEST + 'vendor'))
157 | });
158 |
159 |
160 | /**
161 | * Create a hashed version of all built files. This is currently
162 | * just a placeholder and hasn't been finished yet.
163 | */
164 | gulp.task('build:revisions', ['build:html', 'build:js', 'build:bsless', 'build:style', 'build:images', 'build:fonts', 'build:3rdparty'], function() {
165 | // TODO(gary): Do.
166 | return gulp.src(BUILD_DEST);
167 | });
168 |
169 |
170 | /**
171 | * Super task to build everything.
172 | */
173 | gulp.task('build', ['build:revisions']);
174 |
175 |
176 | /**
177 | * Remove the build directory
178 | */
179 | gulp.task('clean', function(cb) {
180 | del([BUILD_DEST], cb);
181 | });
182 |
183 |
184 | gulp.task('watch', ['build'], function() {
185 | gulp.watch([JS_MAIN_SRC, JS_SRC, STYLE_SRC, IMAGE_SRC, HTML_SRC], ['build']);
186 | });
187 |
--------------------------------------------------------------------------------
/hermes/__init__.py:
--------------------------------------------------------------------------------
1 | from .version import __version__
2 |
--------------------------------------------------------------------------------
/hermes/app.py:
--------------------------------------------------------------------------------
1 |
2 | import tornado
3 | import mimetypes
4 |
5 | from .routes import HANDLERS
6 |
7 | class Application(tornado.web.Application):
8 | def __init__(self, *args, **kwargs):
9 |
10 | kwargs["handlers"] = HANDLERS
11 | self.my_settings = kwargs.pop("my_settings", {})
12 | super(Application, self).__init__(*args, **kwargs)
13 |
--------------------------------------------------------------------------------
/hermes/exc.py:
--------------------------------------------------------------------------------
1 | from tornado.web import HTTPError
2 |
3 | class Error(Exception):
4 | """ Baseclass for Hermes Exceptions."""
5 |
6 | class ModelError(Error):
7 | """ Baseclass for Hermes Model Exceptions."""
8 |
9 | class ValidationError(ModelError):
10 | """ Raised when validation fails on a model."""
11 |
12 | class BaseHttpError(HTTPError):
13 | def __init__(self, log_message, *args, **kwargs):
14 | HTTPError.__init__(
15 | self, self.status_code, log_message, *args, **kwargs
16 | )
17 |
18 | class BadRequest(BaseHttpError): status_code = 400
19 | class Unauthorized(BaseHttpError): status_code = 401
20 | class Forbidden(BaseHttpError): status_code = 403
21 | class NotFound(BaseHttpError): status_code = 404
22 | class Conflict(BaseHttpError): status_code = 409
23 |
--------------------------------------------------------------------------------
/hermes/handlers/__init__.py:
--------------------------------------------------------------------------------
1 | __author__ = 'digant'
2 |
--------------------------------------------------------------------------------
/hermes/handlers/frontends.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging
3 |
4 | from tornado import web
5 |
6 | # Logging object
7 | log = logging.getLogger(__name__)
8 |
9 | class NgApp(web.RequestHandler):
10 | """Our generic handler to serve out the root of our AngularJS app."""
11 | def get(self):
12 | self.render(
13 | os.path.join(os.path.dirname(__file__), "../webapp/build/index.html")
14 | )
15 |
--------------------------------------------------------------------------------
/hermes/handlers/util.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import requests
4 | import sys
5 | from tornado.web import RequestHandler, urlparse, HTTPError
6 | from tornado.escape import utf8
7 | from werkzeug.http import parse_options_header
8 |
9 | from .. import exc
10 | from .. import models
11 | from ..settings import settings
12 |
13 |
14 | # Logging object
15 | log = logging.getLogger(__name__)
16 |
17 | # If raven library is available, modify the base handler to support Sentry.
18 | try:
19 | from raven.contrib.tornado import SentryMixin
20 | except ImportError:
21 | pass
22 | else:
23 | class SentryHandler(SentryMixin, RequestHandler):
24 | pass
25 | RequestHandler = SentryHandler
26 |
27 |
28 | API_VER = "/api/v1"
29 |
30 |
31 | class BaseHandler(RequestHandler):
32 | def initialize(self):
33 |
34 | my_settings = self.application.my_settings
35 |
36 | # Lazily build the engine and session for support of multi-processing
37 | if my_settings.get("db_engine") is None:
38 | my_settings["db_engine"] = models.get_db_engine(my_settings.get("db_uri"))
39 | models.Session.configure(bind=my_settings["db_engine"])
40 | my_settings["db_session"] = models.Session
41 |
42 | self.engine = my_settings.get("db_engine")
43 | self.session = my_settings.get("db_session")()
44 | self.domain = my_settings.get("domain")
45 | self.count_events = my_settings.get("count_events", True)
46 |
47 | def on_finish(self):
48 | self.session.close()
49 |
50 | def get_current_user(self):
51 | """Default global user fetch by user_auth_header."""
52 |
53 | # Fetch the email address from the auth_header (e.g. X-Hermes-Email)
54 | auth_header = settings.user_auth_header
55 | log.debug(' fetching auth_header: %s' % auth_header)
56 | email = self.request.headers.get(auth_header)
57 |
58 | if email is not None:
59 | log.debug('auth_header authenticated user: %s' % email)
60 | return email
61 | return None
62 |
63 | def prepare(self):
64 | log.debug('BaseHandler.prepare()')
65 |
66 |
67 | class FeHandler(BaseHandler):
68 |
69 | def prepare(self):
70 | BaseHandler.prepare(self)
71 | # Need to access token to set Cookie.
72 | # self.xsrf_token
73 |
74 | def render_template(self, template_name, **kwargs):
75 | template = self.application.my_settings["template_env"].get_template(
76 | template_name
77 | )
78 | content = template.render(kwargs)
79 | return content
80 |
81 | def render(self, template_name, **kwargs):
82 | context = {}
83 | context.update(self.get_template_namespace())
84 | context.update(kwargs)
85 | self.write(self.render_template(template_name, **context))
86 |
87 | def write_error(self, status_code, **kwargs):
88 | message = "An unknown problem has occured :("
89 | if "exc_info" in kwargs:
90 | inst = kwargs["exc_info"][1]
91 | if isinstance(inst, HTTPError):
92 | message = inst.log_message
93 | else:
94 | message = str(inst)
95 |
96 | # Pass context to the error template
97 | self.render("error.html", code=status_code, message=message)
98 |
99 |
100 | class ApiHandler(BaseHandler):
101 | def initialize(self):
102 | BaseHandler.initialize(self)
103 | self._jbody = None
104 | self.href_prefix = None
105 |
106 | @property
107 | def jbody(self):
108 | if self._jbody is None:
109 | if self.request.body:
110 | self._jbody = json.loads(self.request.body)
111 | else:
112 | self._jbody = {}
113 | return self._jbody
114 |
115 | def get_pagination_values(self, max_limit=None):
116 | if self.get_arguments("limit"):
117 | if self.get_arguments("limit")[0] == "all":
118 | limit = None
119 | else:
120 | limit = int(self.get_arguments("limit")[0])
121 | else:
122 | limit = 10
123 | offset = int((self.get_arguments("offset") or [0])[0])
124 |
125 | if max_limit is not None and limit > max_limit:
126 | limit = max_limit
127 |
128 | return offset, limit, self.get_arguments("expand")
129 |
130 | def paginate_query(self, query, offset, limit, count=True):
131 | total = None
132 | if count:
133 | total = query.count()
134 |
135 | query = query.offset(offset)
136 | if limit is not None:
137 | query = query.limit(limit)
138 |
139 | return query, total
140 |
141 | def prepare(self):
142 | BaseHandler.prepare(self)
143 |
144 | if self.request.method.lower() in ("put", "post"):
145 | content_type = parse_options_header(
146 | self.request.headers.get("Content-Type")
147 | )[0]
148 | if content_type.lower() != "application/json":
149 | raise exc.BadRequest("Invalid Content-Type for POST/PUT request.")
150 |
151 | self.add_header(
152 | "Content-Type",
153 | "application/json"
154 | )
155 |
156 | self.href_prefix = "{}://{}{}".format(
157 | self.request.protocol,
158 | self.request.host,
159 | API_VER
160 | )
161 |
162 | def not_supported(self):
163 | self.write({
164 | "status": "error",
165 | "error": {
166 | "code": 405,
167 | "message": "Method not supported for this resource."
168 | }
169 | })
170 | self.set_status(405, reason="Method not supported.")
171 |
172 | def write_error(self, status_code, **kwargs):
173 |
174 | message = "An unknown problem has occured :("
175 | if "message" in kwargs:
176 | message = kwargs['message']
177 |
178 | if "exc_info" in kwargs:
179 | inst = kwargs["exc_info"][1]
180 | if isinstance(inst, HTTPError):
181 | message = inst.log_message
182 | else:
183 | message = str(inst)
184 |
185 | self.write({
186 | "status": "error",
187 | "error": {
188 | "code": status_code,
189 | "message": message,
190 | },
191 | })
192 | self.set_status(status_code, message)
193 |
194 | def success(self, data):
195 | """200 OK"""
196 | data['status'] = "ok"
197 | if 'href' not in data:
198 | data['href'] = "{}://{}{}".format(
199 | self.request.protocol,
200 | self.request.host,
201 | self.request.uri
202 | )
203 | self.write(data)
204 | self.finish()
205 |
206 | def created(self, location=None, data=None):
207 | """201 CREATED"""
208 | self.set_status(201)
209 | if data is None:
210 | data = {}
211 | data['status'] = 'created'
212 | if location is not None:
213 | self.set_header(
214 | "Location",
215 | urlparse.urljoin(utf8(self.request.uri), utf8(location))
216 | )
217 | self.write(data)
218 | self.finish()
219 |
--------------------------------------------------------------------------------
/hermes/plugin.py:
--------------------------------------------------------------------------------
1 | """
2 | Module for declaring plugin base classes and helpers.
3 | """
4 |
5 | import annex
6 | import os
7 | import logging
8 |
9 | BUILTIN_PLUGIN_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "plugins")
10 |
11 | log = logging.getLogger(__name__)
12 |
13 | class BaseHermesHook(object):
14 | """ Base class for adding hooks into Hermes.
15 |
16 | This class must be overridden to add actions to perform before and
17 | after particular state transitions in Hermes.
18 | """
19 |
20 | def on_event(self, event):
21 | """Called when an event is created.
22 |
23 | Args:
24 | event: the event that was created
25 | """
26 |
27 |
28 | def get_hooks(additional_dirs=None):
29 | """ Helper function to find and load all hooks. """
30 | log.debug("get_hooks()")
31 | if additional_dirs is None:
32 | additional_dirs = []
33 | hooks = annex.Annex(BaseHermesHook, [
34 | os.path.join(BUILTIN_PLUGIN_DIR, "hooks"),
35 | "/etc/hermes/plugins/hooks",
36 | [os.path.expanduser(os.path.join(plugin_dir, "hooks"))
37 | for plugin_dir in additional_dirs]
38 | ], instantiate=True)
39 |
40 | return hooks
41 |
--------------------------------------------------------------------------------
/hermes/plugins/__init__.py:
--------------------------------------------------------------------------------
1 | __author__ = 'digant'
2 |
--------------------------------------------------------------------------------
/hermes/plugins/hooks/__init__.py:
--------------------------------------------------------------------------------
1 | __author__ = 'digant'
2 |
--------------------------------------------------------------------------------
/hermes/routes.py:
--------------------------------------------------------------------------------
1 | from .handlers import frontends, api
2 | from tornado import web
3 | import os
4 |
5 | HANDLERS = [
6 | # Hosts
7 | (r"/api/v1/hosts\/?", api.HostsHandler),
8 | (r"/api/v1/hosts/(?P.*)\/?", api.HostHandler),
9 |
10 | # Event Types
11 | (r"/api/v1/eventtypes\/?", api.EventTypesHandler),
12 | (r"/api/v1/eventtypes/(?P\d+)\/?", api.EventTypeHandler),
13 |
14 | # Events
15 | (r"/api/v1/events\/?", api.EventsHandler),
16 | (r"/api/v1/events/(?P\d+)\/?", api.EventHandler),
17 |
18 | # Fates
19 | (r"/api/v1/fates\/?", api.FatesHandler),
20 | (r"/api/v1/fates/(?P\d+)\/?", api.FateHandler),
21 |
22 | # Labors
23 | (r"/api/v1/labors\/?", api.LaborsHandler),
24 | (r"/api/v1/labors/(?P\d+)\/?", api.LaborHandler),
25 |
26 | # Quests
27 | (r"/api/v1/quests\/?", api.QuestsHandler),
28 | (r"/api/v1/quests/(?P\d+)\/?", api.QuestHandler),
29 | (r"/api/v1/quests/(?P\d+)/mail\/?", api.QuestMailHandler),
30 |
31 | # Queries to 3rd party tools
32 | (r"/api/v1/extquery\/?", api.ExtQueryHandler),
33 |
34 | # Query for the current user
35 | (r"/api/v1/currentUser", api.CurrentUserHandler),
36 |
37 | # Query the server for its configs
38 | (r"/api/v1/serverConfig", api.ServerConfig),
39 |
40 | # Frontend Handlers
41 | (
42 | r"/((?:css|fonts|img|js|vendor|templates)/.*)",
43 | web.StaticFileHandler,
44 | dict(
45 | path=os.path.join(os.path.dirname(__file__), "webapp/build")
46 | )
47 | ),
48 |
49 | # Frontend Handlers
50 | (
51 | r"/.*",
52 | frontends.NgApp
53 | )
54 | ]
55 |
--------------------------------------------------------------------------------
/hermes/settings.py:
--------------------------------------------------------------------------------
1 | import yaml
2 |
3 |
4 | class Settings(object):
5 | def __init__(self, initial_settings):
6 | self.settings = initial_settings
7 |
8 | @classmethod
9 | def from_settings(cls, settings, initial_settings=None):
10 | _settings = {}
11 | _settings.update(settings.settings)
12 | if initial_settings:
13 | _settings.update(initial_settings)
14 | return cls(_settings)
15 |
16 | def update_from_config(self, filename):
17 | with open(filename) as config:
18 | data = yaml.safe_load(config.read())
19 |
20 | settings = {}
21 | settings.update(data)
22 |
23 | for key, value in settings.iteritems():
24 | key = key.lower()
25 |
26 | if key not in self.settings:
27 | continue
28 |
29 | override = getattr(self, "override_%s" % key, None)
30 | if override is not None and callable(override):
31 | value = override(value)
32 |
33 | self.settings[key] = value
34 |
35 | def __getitem__(self, key):
36 | return self.settings[key]
37 |
38 | def __getattr__(self, name):
39 | try:
40 | return self.settings[name]
41 | except KeyError as err:
42 | raise AttributeError(err)
43 |
44 |
45 | settings = Settings({
46 | "log_format": "%(asctime)-15s\t%(levelname)s\t%(message)s",
47 | "num_processes": 1,
48 | "database": None,
49 | "query_server": "http://localhost:5353/api/query",
50 | "frontend": "https://hermes.company.net",
51 | "slack_webhook": None,
52 | "slack_proxyhost": None,
53 | "debug": False,
54 | "domain": "localhost",
55 | "port": 8990,
56 | "user_auth_header": "X-Hermes-Email",
57 | "email_notifications": False,
58 | "email_sender_address": "hermes@localhost",
59 | "email_always_copy": "",
60 | "restrict_networks": [],
61 | "bind_address": None,
62 | "api_xsrf_enabled": True,
63 | "secret_key": "SECRET_KEY",
64 | "auth_token_expiry": 600, # 10 minutes
65 | "sentry_dsn": None,
66 | "plugin_dir": "plugins ",
67 | "environment": "dev",
68 | "dev_email_recipient": "",
69 | "fullstory_id": None,
70 | "strongpoc_server": None,
71 | "count_events": True,
72 | })
73 |
--------------------------------------------------------------------------------
/hermes/settings_client.py:
--------------------------------------------------------------------------------
1 | import yaml
2 |
3 |
4 | class Settings(object):
5 | def __init__(self, initial_settings):
6 | self.settings = initial_settings
7 |
8 | @classmethod
9 | def from_settings(cls, settings, initial_settings=None):
10 | _settings = {}
11 | _settings.update(settings.settings)
12 | if initial_settings:
13 | _settings.update(initial_settings)
14 | return cls(_settings)
15 |
16 | def update_from_config(self, filename):
17 | with open(filename) as config:
18 | data = yaml.safe_load(config.read())
19 |
20 | settings = {}
21 | settings.update(data)
22 |
23 | for key, value in settings.iteritems():
24 | key = key.lower()
25 |
26 | if key not in self.settings:
27 | continue
28 |
29 | override = getattr(self, "override_%s" % key, None)
30 | if override is not None and callable(override):
31 | value = override(value)
32 |
33 | self.settings[key] = value
34 |
35 | def __getitem__(self, key):
36 | return self.settings[key]
37 |
38 | def __getattr__(self, name):
39 | try:
40 | return self.settings[name]
41 | except KeyError as err:
42 | raise AttributeError(err)
43 |
44 |
45 | settings = Settings({
46 | "log_format": "%(asctime)-15s\t%(levelname)s\t%(message)s",
47 | "debug": False,
48 | "user_auth_header": "X-Hermes-Email",
49 | "hermes_server": "http://localhost:10901",
50 | "api_retries": 5
51 | })
52 |
--------------------------------------------------------------------------------
/hermes/util.py:
--------------------------------------------------------------------------------
1 | """
2 | Project-wide utilities.
3 | """
4 |
5 | import logging
6 | import random
7 | import requests
8 | import smtplib
9 | import string
10 |
11 | from email.mime.text import MIMEText
12 | from email.mime.multipart import MIMEMultipart
13 |
14 | from .settings import settings
15 |
16 |
17 | log = logging.getLogger(__name__)
18 |
19 |
20 | def id_generator(size=6, chars=string.ascii_lowercase + string.digits):
21 | """Generate a random ID of specified length
22 |
23 | Args:
24 | size: the length of the id to generate
25 | chars: the characters to use
26 |
27 | Returns:
28 | string of random id generated
29 | """
30 | return ''.join(random.choice(chars) for _ in range(size))
31 |
32 |
33 | def slack_message(message):
34 | """Post a message to Slack if a webhook as been defined.
35 |
36 | Args:
37 | message: the content of the Slack post
38 | """
39 | if not settings.slack_webhook:
40 | return
41 |
42 | if settings.slack_proxyhost:
43 | proxies = {
44 | "http": "http://{}".format(settings.slack_proxyhost),
45 | "https": "http://{}".format(settings.slack_proxyhost)
46 | }
47 | else:
48 | proxies = None
49 |
50 | json = {
51 | "text": message,
52 | "username": "Hermes Log",
53 | "icon_emoji": ":hermes:",
54 | }
55 | try:
56 | log.debug("{} {}".format(settings.slack_webhook, json))
57 | response = requests.post(
58 | settings.slack_webhook, json=json, proxies=proxies
59 | )
60 | except Exception as exc:
61 | log.warn("Error writing to Slack: {}".format(exc.message))
62 |
63 |
64 | def email_message(recipients, subject, message, html_message=None, cc=None, sender=None):
65 | """Email a message to a user.
66 |
67 | Args:
68 | subject: the subject of the email we wish to send
69 | message: the content of the email we wish to send
70 | recipients: the email address to whom we wish to send the email
71 | html_message: optional html formatted message we wish to send
72 | cc: optional list of email addresses to carbon copy
73 | sender: optional sender email address
74 | """
75 |
76 | if not settings.email_notifications:
77 | return
78 |
79 | if isinstance(recipients, basestring):
80 | recipients = recipients.split(",")
81 | if isinstance(settings.email_always_copy, basestring):
82 | extra_recipients = settings.email_always_copy.split(",")
83 | else:
84 | extra_recipients = [settings.email_always_copy]
85 |
86 | if cc and isinstance(cc, basestring):
87 | extra_recipients.append(cc)
88 | elif cc:
89 | extra_recipients.extend(cc)
90 |
91 | # If this is the dev environment, we need to only send to the dev recipient
92 | # and put a tag explaining what would have happened
93 |
94 | if settings.environment == "dev":
95 | recipients_statement = "To: {} CC: {}\n".format(
96 | recipients, extra_recipients
97 | )
98 | subject = "[DEV] {}".format(subject)
99 | message = (
100 | "[DEV]: Sent to {}\nOriginally addressed as: {}\n\n{}".format(
101 | settings.dev_email_recipient,
102 | recipients_statement,
103 | message
104 | )
105 | )
106 | if html_message:
107 | html_message = (
108 | "DEV: Sent to {} "
109 | "Originally addressed as: {}
{}".format(
110 | settings.dev_email_recipient,
111 | recipients_statement,
112 | html_message
113 | )
114 | )
115 | recipients = [settings.dev_email_recipient]
116 | extra_recipients = []
117 |
118 | part1 = MIMEText(message, 'plain')
119 | if html_message:
120 | part2 = MIMEText(html_message, 'html')
121 | else:
122 | part2 = None
123 |
124 | if part1 and part2:
125 | msg = MIMEMultipart('alternative')
126 | msg.attach(part1)
127 | msg.attach(part2)
128 | else:
129 | msg = part1
130 |
131 | msg["Subject"] = subject
132 | msg["From"] = settings.email_sender_address if not sender else sender
133 | msg["To"] = ", ".join(recipients)
134 | msg["Cc"] = ", ".join(extra_recipients)
135 |
136 | logging.debug("Sending email: From {}, To {}, Msg: {}".format(
137 | settings.email_sender_address,
138 | recipients + extra_recipients,
139 | msg.as_string()
140 | ))
141 |
142 | try:
143 | smtp = smtplib.SMTP("localhost")
144 | smtp.sendmail(
145 | settings.email_sender_address,
146 | recipients + extra_recipients,
147 | msg.as_string()
148 | )
149 | smtp.quit()
150 | except Exception as exc:
151 | log.warn("Error sending email: {}".format(exc.message))
152 |
153 |
154 | class PluginHelper(object):
155 | @classmethod
156 | def request_get(cls, path="", params={}, server=None):
157 | """Make an HTTP GET request for the given path
158 |
159 | Args:
160 | path: the full path to the resource
161 | params: the query parameters to send
162 | server: the server to talk to, default is query_server
163 | Returns:
164 | the http response
165 | """
166 |
167 | if not server:
168 | server = settings.query_server
169 |
170 | response = requests.get(server + path, params=params)
171 |
172 | return response
173 |
174 | @classmethod
175 | def request_post(cls, path="", params={}, json_body={}, server=None):
176 | """Make an HTTP POST request for the given path
177 |
178 | Args:
179 | path: the full path to the resource
180 | params: the query params to send
181 | json_body: the body of the message in JSON format
182 | server: the server to talk to, default is query_server
183 | Returns:
184 | the http response
185 | """
186 |
187 | if not server:
188 | server = settings.query_server
189 |
190 | response = requests.post(
191 | server + path, params=params, json=json_body
192 | )
193 |
194 | return response
195 |
--------------------------------------------------------------------------------
/hermes/version.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.7.27"
2 |
--------------------------------------------------------------------------------
/hermes/webapp/src/img/icon.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dropbox/hermes/36e5c0532571eed42d5f6edea35f755b837a5b2d/hermes/webapp/src/img/icon.gif
--------------------------------------------------------------------------------
/hermes/webapp/src/img/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dropbox/hermes/36e5c0532571eed42d5f6edea35f755b837a5b2d/hermes/webapp/src/img/loading.gif
--------------------------------------------------------------------------------
/hermes/webapp/src/img/loading_15.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dropbox/hermes/36e5c0532571eed42d5f6edea35f755b837a5b2d/hermes/webapp/src/img/loading_15.gif
--------------------------------------------------------------------------------
/hermes/webapp/src/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dropbox/hermes/36e5c0532571eed42d5f6edea35f755b837a5b2d/hermes/webapp/src/img/logo.png
--------------------------------------------------------------------------------
/hermes/webapp/src/img/remove_10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dropbox/hermes/36e5c0532571eed42d5f6edea35f755b837a5b2d/hermes/webapp/src/img/remove_10.png
--------------------------------------------------------------------------------
/hermes/webapp/src/img/remove_15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dropbox/hermes/36e5c0532571eed42d5f6edea35f755b837a5b2d/hermes/webapp/src/img/remove_15.png
--------------------------------------------------------------------------------
/hermes/webapp/src/img/send_email_15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dropbox/hermes/36e5c0532571eed42d5f6edea35f755b837a5b2d/hermes/webapp/src/img/send_email_15.png
--------------------------------------------------------------------------------
/hermes/webapp/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hermes
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
38 |
39 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/hermes/webapp/src/js/controllers/questCreationCtrl.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | function QuestCreationCtrl(hermesService, $q, $routeParams, $location) {
5 | var vm = this;
6 |
7 | vm.user = null;
8 | vm.hostList = [];
9 | vm.selectedFate = null;
10 | vm.description = null;
11 | vm.today = new Date();
12 | vm.targetDate = new Date();
13 | vm.targetDate.setDate(new Date().getDate() + 14);
14 | vm.targetDate.setTime(Math.round(vm.targetDate.getTime() / 900000) * 900000);
15 |
16 | vm.errorMessages = null;
17 | vm.addHostErrorMessages = [];
18 | vm.queryErrorMessage = null;
19 | vm.successMessage = null;
20 | vm.createInProgress = false;
21 | vm.queryInProgress = false;
22 | vm.result = null;
23 | vm.showFatesModal = false;
24 | vm.queryString = null;
25 | vm.hostNameEntry = null;
26 |
27 | vm.queriedHosts = [];
28 | vm.startingEventTypes = null;
29 | vm.startingFates = [];
30 |
31 | vm.selectOptions = {
32 | updateOn: 'default change blur',
33 | getterSetter: true,
34 | allowInvalid: true
35 | };
36 |
37 |
38 | hermesService.getFates().then(function(fates) {
39 | vm.startingFates = [];
40 | for (var idx in fates) {
41 | if (!fates[idx]['followsId']) {
42 | vm.startingFates.push(fates[idx]);
43 | }
44 | }
45 | if (vm.startingFates.length != 0) {
46 | vm.selectedFate = vm.startingFates[0];
47 | }
48 | });
49 |
50 | hermesService.getCurrentUser().then(function(user){
51 | if (user) {
52 | vm.user = user;
53 | } else {
54 | vm.errorMessages.push("Cannot create a new quest if not authenticated.");
55 | }
56 | });
57 |
58 | vm.runQuery = runQuery;
59 | vm.removeQueriedHost = removeQueriedHost;
60 | vm.removeHost = removeHost;
61 | vm.addHost = addHost;
62 | vm.moveQueriedToQueued = moveQueriedToQueued;
63 | vm.fateSelection = fateSelection;
64 | vm.createQuest = createQuest;
65 | vm.calDateClasser = calDateClasser;
66 |
67 |
68 | ////////////////////////////////
69 |
70 | /**
71 | * Create a quest with the information we have
72 | */
73 | function createQuest() {
74 | if (vm.createInProgress) return;
75 |
76 | vm.createInProgress = true;
77 |
78 | vm.errorMessages = [];
79 |
80 | if (vm.hostList.length == 0) {
81 | vm.errorMessages.push("Cannot create a quest with an empty list of hosts.");
82 | }
83 |
84 | if (!vm.selectedFate) {
85 | vm.errorMessages.push("Cannot create a quest without a starting fate.");
86 | }
87 |
88 | if (!vm.description) {
89 | vm.errorMessages.push("Cannot create a quest without a description.");
90 | }
91 |
92 | if (!vm.user) {
93 | vm.errorMessages.push("Cannot create a new quest if not authenticated.");
94 | }
95 |
96 | if (vm.errorMessages.length != 0) {
97 | vm.createInProgress = false;
98 | return;
99 | }
100 |
101 | vm.result = hermesService.createQuest(vm.user, vm.hostList,
102 | vm.selectedFate.id, vm.targetDate, vm.description)
103 | .then(function(response) {
104 | vm.createInProgress = false;
105 | vm.hostList = [];
106 | vm.description = null;
107 | vm.successMessage = "Successfully create quest " + response.data.id;
108 | })
109 | .catch(function(error) {
110 | vm.createInProgress = false;
111 | vm.errorMessages.push("Quest creation failed! " + error.statusText);
112 | });
113 |
114 | }
115 |
116 | /**
117 | * The getter/setter for event types
118 | */
119 | function fateSelection(selection) {
120 | if (angular.isDefined(selection)) {
121 | vm.selectedFate = selection;
122 | } else {
123 | return vm.selectedFate;
124 | }
125 |
126 | }
127 |
128 | /**
129 | * Run the user specified query against the query passthrough service
130 | */
131 | function runQuery() {
132 | if (!vm.queryString || vm.queryString.trim().length == 0) {
133 | vm.queryErrorMessage = "Query is empty.";
134 | return;
135 | }
136 | vm.queryErrorMessage = null;
137 | vm.queryInProgress = true;
138 | hermesService.runQuery(vm.queryString).then(function(hosts) {
139 | vm.queryInProgress = false;
140 | if (hosts && hosts.length != 0) {
141 | vm.queriedHosts = hosts;
142 | } else {
143 | vm.queryErrorMessage = "Query returned no results.";
144 | }
145 | }).catch(function(error) {
146 | vm.queryInProgress = false;
147 | vm.queryErrorMessage = "Failed to run query! " + error.statusText;
148 | });
149 | }
150 |
151 | /**
152 | * Remove a host from the list of hosts generated by the query
153 | * @param host the host to remove
154 | */
155 | function removeQueriedHost(host) {
156 | var idx = vm.queriedHosts.indexOf(host);
157 | if (idx > -1) {
158 | vm.queriedHosts.splice(idx, 1);
159 | }
160 | }
161 |
162 | /**
163 | * Remove a host from the list of hosts queued up for this quest
164 | * @param host the host to remove
165 | */
166 | function removeHost(host) {
167 | var idx = vm.hostList.indexOf(host);
168 | if (idx > -1) {
169 | vm.hostList.splice(idx, 1);
170 | }
171 | }
172 |
173 | /**
174 | * Add a specified host to the hosts queued up, but only if it isn't
175 | * in there already
176 | * @param host the host to add
177 | */
178 | function addHost(host) {
179 | vm.addHostErrorMessages = [];
180 | if (!host) {
181 | vm.addHostErrorMessages.push("Hostname empty.");
182 | return;
183 | }
184 |
185 | var hosts = host.split(",");
186 | for (var idx in hosts) {
187 | var host = hosts[idx].trim();
188 | if (vm.hostList.indexOf(host) == -1) {
189 | vm.hostList.push(host);
190 | } else {
191 | vm.addHostErrorMessages.push("Ignoring duplicate host " + host);
192 | }
193 | }
194 | }
195 |
196 | /**
197 | * Add queried hosts to queued hosts
198 | */
199 | function moveQueriedToQueued() {
200 | while (vm.queriedHosts.length > 0) {
201 | addHost(vm.queriedHosts.shift());
202 | }
203 | }
204 |
205 | /**
206 | * Adds our classes to the date picker
207 | * @param date the date in question
208 | * @param mode the mode
209 | */
210 | function calDateClasser(date, mode) {
211 | return "date-picker";
212 | }
213 |
214 | }
215 |
216 | angular.module('hermesApp').controller('QuestCreationCtrl', QuestCreationCtrl);
217 | QuestCreationCtrl.$inject = ['HermesService', '$q', '$routeParams', '$location'];
218 | })();
219 |
--------------------------------------------------------------------------------
/hermes/webapp/src/js/controllers/questEditCtrl.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | function QuestEditCtrl(hermesService, $q, $routeParams, $location) {
5 | var vm = this;
6 |
7 | vm.user = null; // holds the current user; we should only let current owners update quests
8 | vm.today = new Date(); // used to restrict how far back the due date can be set
9 | vm.quest = null; // holds the retrieve quest details
10 |
11 | // new values that the user can edit
12 | vm.newCreator = null;
13 | vm.description = null;
14 | vm.targetDate = null;
15 |
16 | vm.editingCreator = false; // controls if we show the creator editing field
17 | vm.editingDate = false; // controls if we show the date picker
18 | vm.editingDesc = false; // controls if we show the desc edit field
19 |
20 | // various messaging fields
21 | vm.successMessage = null;
22 | vm.errorMessage = null;
23 |
24 | hermesService.getCurrentUser().then(function(user){
25 | if (user) {
26 | vm.user = user;
27 | } else {
28 | vm.errorMessages.push("Cannot create a new quest if not authenticated.");
29 | }
30 | });
31 |
32 | refreshQuestInfo();
33 |
34 | vm.calDateClasser = calDateClasser;
35 | vm.focus = focus;
36 | vm.saveCreator = saveCreator;
37 | vm.saveTargetTime = saveTargetTime;
38 | vm.saveDescription = saveDescription;
39 | vm.refreshQuestInfo = refreshQuestInfo;
40 |
41 | ////////////////////////////////
42 |
43 | function refreshQuestInfo() {
44 | hermesService.getQuestDetails($routeParams.questId).then(function(quest) {
45 | vm.quest = quest;
46 | vm.description = quest.description;
47 | vm.targetDate = new Date();
48 | //vm.targetDate.setDate(new Date(targetDate).getDate());
49 | vm.targetDate.setTime(Math.round(vm.targetDate.getTime() / 900000) * 900000);
50 | });
51 | }
52 |
53 | /**
54 | * Adds our classes to the date picker
55 | * @param date the date in question
56 | * @param mode the mode
57 | */
58 | function calDateClasser(date, mode) {
59 | return "date-picker";
60 | }
61 |
62 | /**
63 | * Helper to set focus and select the text
64 | * @param id the item that gets focus
65 | */
66 | function focus(id) {
67 | setTimeout(function() {
68 | document.getElementById(id).focus();
69 | document.getElementById(id).select();
70 | }, 10);
71 | }
72 |
73 | /**
74 | * Change the creator for a quest
75 | */
76 | function saveCreator() {
77 | hermesService.updateQuest(vm.quest.id, {"creator": vm.newCreator})
78 | .then(function(response) {
79 | vm.successMessage = "Updated creator to " + vm.newCreator;
80 | vm.newCreator = null;
81 | vm.editingCreator = false;
82 | vm.refreshQuestInfo();
83 | })
84 | .catch(function(error) {
85 | vm.errorMessage = "Error updating creator: " + error.statusText;
86 | })
87 | }
88 |
89 | /**
90 | * Change the target time for a quest
91 | */
92 | function saveTargetTime() {
93 | hermesService.updateQuest(vm.quest.id, {"targetTime": vm.targetDate})
94 | .then(function(response) {
95 | vm.successMessage = "Updated target time to " + vm.targetDate;
96 | vm.editingDate = false;
97 | vm.refreshQuestInfo();
98 | })
99 | .catch(function(error) {
100 | vm.errorMessage = "Error updating target time: " + error.statusText;
101 | })
102 | }
103 |
104 | /**
105 | * Change the description for a quest
106 | */
107 | function saveDescription() {
108 | hermesService.updateQuest(vm.quest.id, {"description": vm.description})
109 | .then(function(response) {
110 | vm.successMessage = "Updated description!";
111 | vm.editingDesc = false;
112 | vm.refreshQuestInfo();
113 | })
114 | .catch(function(error) {
115 | vm.errorMessage = "Error updating description: " + error.statusText;
116 | })
117 | }
118 |
119 | }
120 |
121 | angular.module('hermesApp').controller('QuestEditCtrl', QuestEditCtrl);
122 | QuestEditCtrl.$inject = ['HermesService', '$q', '$routeParams', '$location'];
123 | })();
124 |
--------------------------------------------------------------------------------
/hermes/webapp/src/js/controllers/userHomeCtrl.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | function UserHomeCtrl(hermesService, $q, $routeParams, $location, smoothScroll) {
5 | var vm = this;
6 |
7 | vm.errorMessage = null;
8 |
9 | vm.domain = null;
10 | vm.user = null;
11 | vm.questData = null;
12 | vm.totalQuests = null;
13 | vm.totalUserCreatedQuests = null;
14 | vm.totalLabors = null;
15 | vm.totalUserLabors = null;
16 |
17 | vm.questsUrl = null;
18 | vm.laborsUrl = null;
19 |
20 | vm.goToQuestsPage = goToQuestsPage;
21 | vm.goToLaborsPage = goToLaborsPage;
22 |
23 | hermesService.getCurrentUser().then(function (user) {
24 | if (user) {
25 | vm.user = user;
26 | }
27 |
28 | // find labors and quests for this user
29 | getOpenQuests();
30 | getOpenLabors();
31 | });
32 |
33 | hermesService.getServerConfig().then(function(config) {
34 | vm.domain = config['domain'];
35 | });
36 |
37 | function goToCreatePage() {
38 | $location.url("/v1/quest/new");
39 | }
40 |
41 | function goToQuestsPage() {
42 | $location.url(vm.questsUrl);
43 | }
44 |
45 | function goToLaborsPage() {
46 | $location.url(vm.laborsUrl);
47 | }
48 |
49 | /**
50 | * Get open quest information, but we only want basic overview information.
51 | */
52 | function getOpenQuests() {
53 | vm.errorMessage = null;
54 |
55 | var options = {};
56 | options['overviewOnly'] = true;
57 |
58 | hermesService.getOpenQuests(options).then(function (questData) {
59 | vm.questData = questData['quests'];
60 | vm.totalQuests = questData['totalQuests'];
61 |
62 | // see which quests are overdue and which are owned by this user
63 | vm.totalUserCreatedQuests = 0;
64 | for (var idx in vm.questData) {
65 | evalDueDate(vm.questData[idx]);
66 | if (vm.questData[idx]['creator'] == vm.user) {
67 | vm.totalUserCreatedQuests++;
68 | }
69 | }
70 |
71 | if (vm.totalUserCreatedQuests == 0) {
72 | vm.questsUrl = "/v1/quests/?byCreator=";
73 | } else {
74 | vm.questsUrl = "/v1/quests?byCreator=" + vm.user;
75 | }
76 | });
77 | }
78 |
79 | /**
80 | * Get labor information (overview only) for all open labors and labors
81 | * that apply to this user.
82 | */
83 | function getOpenLabors() {
84 | var options = {};
85 | options['overviewOnly'] = true;
86 |
87 | hermesService.getOpenLabors(options).then(function (laborData){
88 | vm.totalLabors = laborData['totalLabors'];
89 | });
90 |
91 | options['filterByOwner'] = vm.user;
92 |
93 | hermesService.getOpenLabors(options).then(function (laborData) {
94 | vm.totalUserLabors = laborData['totalLabors'];
95 | vm.laborsUrl = "/v1/labors?byOwner=" + vm.user;
96 | }).catch(function(error) {
97 | vm.totalUserLabors = 0;
98 | vm.laborsUrl = "/v1/labors?byOwner=";
99 | });
100 | }
101 |
102 | /**
103 | * Determine if the quest is overdue and add a property to indicate
104 | * @param quest the quest to analyze
105 | */
106 | function evalDueDate(quest) {
107 | if (quest.targetTime) {
108 | var dateRegex = /(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/;
109 | var dateArray = dateRegex.exec(quest.targetTime);
110 | var targetDate = new Date(
111 | (+dateArray[1]),
112 | (+dateArray[2]) - 1, // Careful, month starts at 0!
113 | (+dateArray[3]),
114 | (+dateArray[4]),
115 | (+dateArray[5]),
116 | (+dateArray[6])
117 | );
118 |
119 | if (targetDate - new Date() <= 0) quest.overDue = true;
120 | else quest.overDue = false;
121 | } else {
122 | quest.overDue = false;
123 | }
124 | }
125 | }
126 |
127 | angular.module('hermesApp').controller('UserHomeCtrl', UserHomeCtrl);
128 | UserHomeCtrl.$inject = ['HermesService', '$q', '$routeParams', '$location', 'smoothScroll'];
129 | })();
130 |
--------------------------------------------------------------------------------
/hermes/webapp/src/js/directives/boxContinuation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Directive for watching a div and adding a "box-continuation" class when a
3 | * particular height is reached. This is used when we have a max-height for a div
4 | * with a scroll and want to clue the user in to the fact that the box has additional
5 | * items in it.
6 | *
7 | * A "watch" attribute must be specified that let's us know what data
8 | * modifies the contents of the div (and therefore might alter the height).
9 | */
10 | (function() {
11 | function boxContinuation () {
12 | return {
13 | restrict: 'A',
14 | scope: {
15 | 'watch': '='
16 | },
17 | link: function ($scope, $ele, $attrs) {
18 | var triggerHeight = $attrs.triggerHeight || 200;
19 |
20 | $scope.$watch('watch',
21 | classAlteration, true);
22 |
23 | function classAlteration() {
24 | setTimeout(function() {
25 | if ($ele[0].clientHeight >= triggerHeight) {
26 | $ele[0].classList.add('box-continuation');
27 | } else {
28 | $ele[0].classList.remove('box-continuation');
29 | }
30 | }, 1);
31 | }
32 | }
33 | }
34 | }
35 |
36 | angular.module('hermesApp').directive('boxContinuation', boxContinuation);
37 | })();
--------------------------------------------------------------------------------
/hermes/webapp/src/js/directives/questProgressBar.js:
--------------------------------------------------------------------------------
1 | /**
2 | * directive for building quest progress bars with Raphael
3 | */
4 | (function() {
5 | function questProgressBar ($timeout, $window) {
6 | return {
7 | restrict: 'A',
8 | scope: {
9 | data: '=',
10 | onClick: '&'
11 | },
12 | link: function ($scope, $ele, $attrs) {
13 | var renderTimeout;
14 | var lastData;
15 | var isVisible = true;
16 | var graphHeight = parseInt($attrs.graphHeight) || 30;
17 | var raphael = new Raphael($ele[0], "100%", graphHeight);
18 |
19 | $scope.$watch('data', function (newData) {
20 | lastData = [newData];
21 | $scope.render([newData]);
22 | }, true);
23 |
24 | $window.onresize = function () {
25 | $scope.$apply();
26 | };
27 |
28 | $scope.$watch(function () {
29 | return angular.element($window)[0].innerWidth;
30 | }, function () {
31 | $scope.render([$scope.data]);
32 | });
33 |
34 | $scope.render = function (data) {
35 | if (!data || !isVisible) return;
36 | if (renderTimeout) clearTimeout(renderTimeout);
37 |
38 | renderTimeout = $timeout(function () {
39 | var width = $ele[0].offsetWidth;
40 | var topPadding = graphHeight * .05;
41 | var barWidth = width;
42 | var barBaseLine = graphHeight * 1;
43 | var barHeight = graphHeight * .3;
44 | var percentXLoc = data[0].percentComplete * barWidth / 100;
45 |
46 | var colors = ['#7b8994','#3d464d'];
47 | var textColor = '#3d464d';
48 |
49 | if (data[0].overDue) {
50 | colors = ['#944D4D', '#6A1C0E'];
51 | textColor = '#6A1C0E';
52 | }
53 |
54 | // erase everything
55 | raphael.clear();
56 |
57 | // draw the full bar, which shows the full extent of the progress bar
58 | var fullBar = raphael.rect(
59 | 0, barBaseLine - barHeight,
60 | barWidth, barHeight
61 | );
62 | fullBar.attr({'fill': colors[0]});
63 | fullBar.attr({'stroke-width': 0});
64 |
65 | // color in the part to represent the percentage complete
66 | var percentBar = raphael.rect(
67 | 0, barBaseLine - barHeight,
68 | percentXLoc, barHeight
69 | );
70 | percentBar.attr({'fill': colors[1]});
71 | percentBar.attr({'stroke-width': 0});
72 |
73 | // draw the little line that points up to the percentage label
74 | var pathStr = "M" + percentXLoc + "," + (barBaseLine - (barHeight * 1.5))
75 | + " L" + percentXLoc + "," + barBaseLine;
76 | raphael.path(pathStr).attr({'stroke': colors[1]});
77 |
78 | // add the percentage amount text
79 | var label = raphael.text(
80 | width/2,
81 | (barBaseLine - (barHeight * 2)),
82 | data[0].percentComplete + "%"
83 | ).attr({
84 | 'font-size': barHeight,
85 | 'fill': textColor,
86 | 'font-family': "Titillium Web",
87 | });
88 |
89 | var bb = label.getBBox();
90 | var labelWidth = Math.abs(bb.x2) - Math.abs(bb.x) + 1;
91 | if (percentXLoc - (labelWidth / 2) <= 0) {
92 | label.attr({'x': labelWidth / 2});
93 | } else if (percentXLoc + (labelWidth / 2) >= barWidth) {
94 | label.attr({'x': barWidth - (labelWidth/2)});
95 | } else {
96 | label.attr({'x': percentXLoc});
97 | }
98 |
99 | }, 0);
100 | };
101 | }
102 | }
103 | }
104 |
105 | angular.module('hermesApp').directive('questProgressBar', questProgressBar);
106 | questProgressBar.$inject = ['$timeout', '$window'];
107 | })();
--------------------------------------------------------------------------------
/hermes/webapp/src/js/directives/questProgressChart.js:
--------------------------------------------------------------------------------
1 | /**
2 | * directive for building quest progress bars with Raphael
3 | */
4 | (function() {
5 | function questProgressChart (hermesService, $timeout, $window) {
6 | return {
7 | restrict: 'A',
8 | scope: {
9 | data: '=',
10 | types: '=',
11 | colors: '=',
12 | onClick: '&'
13 | },
14 | link: function ($scope, $ele, $attrs) {
15 | var width = $ele[0].offsetWidth;
16 | var graphHeight = 200;
17 | var legendFontSize = graphHeight * .06;
18 | var legendSpacing = graphHeight * .09;
19 | var titleFontSize = graphHeight * .12;
20 |
21 |
22 | var renderTimeout;
23 | var types = null;
24 | var numberOfTypes = 0;
25 | var rad = Math.PI / 180;
26 |
27 | // we don't pull this from the backend b/c the backend gives a total that
28 | // includes completed intermediate labors
29 | var totalLabors = 0;
30 | var colors;
31 | var raphael = new Raphael($ele[0], "100%", graphHeight);
32 |
33 | var graphData = null;
34 | hermesService.getFatesGraph().then(function(data) {
35 | graphData = data;
36 | });
37 |
38 | $scope.$watch('data', function (newData) {
39 | $scope.render([newData]);
40 | }, true);
41 |
42 | $scope.$watch('types', function (newData) {
43 | types = newData;
44 | numberOfTypes = 0;
45 | totalLabors = 0;
46 | for (var idx in types) {
47 | numberOfTypes++;
48 | totalLabors += types[idx];
49 | }
50 | });
51 |
52 | $window.onresize = function () {
53 | $scope.$apply();
54 | };
55 |
56 | $scope.$watch(function () {
57 | return angular.element($window)[0].innerWidth;
58 | }, function () {
59 | $scope.render([$scope.data]);
60 | });
61 |
62 |
63 | $scope.$watch('colors', function (newData) {
64 | colors = newData;
65 | }, true);
66 |
67 | $scope.render = function (data) {
68 | if (!data) return;
69 | if (renderTimeout) clearTimeout(renderTimeout);
70 |
71 | function wrapText(text, textEle, maxWidth) {
72 | text = text.replace(/\s+/g, ' ').trim();
73 | text = text.replace('\n', ' ');
74 | var words = text.split(" ");
75 | var wrappedText = '';
76 | for (var idx in words) {
77 | textEle.attr("text", wrappedText + " " + words[idx]);
78 | if (textEle.getBBox().width > maxWidth) {
79 | wrappedText += '\n' + words[idx];
80 | } else {
81 | wrappedText += ' ' + words[idx];
82 | }
83 | }
84 |
85 | var bb = textEle.getBBox();
86 | var h = Math.abs(bb.y2) - Math.abs(bb.y) + 1;
87 | textEle.attr({
88 | 'y': bb.y + h
89 | });
90 | }
91 |
92 | renderTimeout = $timeout(function () {
93 | var width = $ele[0].offsetWidth;
94 | var legendX = width * .70;
95 | var legendY = ((graphHeight *.9) + ((numberOfTypes-1)* legendSpacing)) / 2;
96 | var pieX = width * .5;
97 | var pieY = graphHeight * .5;
98 | var pieR = graphHeight * .45;
99 |
100 | // erase everything
101 | raphael.clear();
102 |
103 | // add the quest info to the top left
104 | var title = raphael.text(0, titleFontSize,
105 | "Quest " + data[0].id)
106 | .attr({
107 | 'text-anchor': 'start',
108 | 'font-size': titleFontSize,
109 | 'font-family': "Titillium Web"
110 | });
111 |
112 | var creator = raphael.text(0, titleFontSize
113 | + legendFontSize * 2,
114 | "Created by: " + data[0].creator)
115 | .attr({
116 | 'text-anchor': 'start',
117 | 'font-size': legendFontSize,
118 | 'font-family': "Titillium Web"
119 | });
120 |
121 | // add the quest description
122 | var desc = raphael.text(0, legendY - legendSpacing /1.5)
123 | .attr({
124 | 'text-anchor': 'start',
125 | 'font-size': legendFontSize,
126 | 'font-family': "Titillium Web"
127 | });
128 |
129 | wrapText(data[0].description, desc, width *.35);
130 |
131 | if (data[0].overDue) {
132 | raphael.text(legendX, titleFontSize, "OVERDUE")
133 | .attr({
134 | 'font-size': titleFontSize,
135 | 'font-family': "Titillium Web",
136 | 'fill': "#953D2D",
137 | 'text-anchor': 'start'
138 | });
139 | }
140 |
141 | // draw out the legend on the right
142 | var i = 0;
143 | var lastAngle = 0;
144 | for (var idx in types) {
145 | var type = idx;
146 | var x = legendX;
147 | var y = legendY + (i * legendSpacing * 1.1);
148 | var text = raphael.text(
149 | x, y, type
150 | ).attr({
151 | 'font-size': legendFontSize,
152 | 'font-family': "Titillium Web",
153 | 'text-anchor': 'start'
154 | });
155 |
156 | var boxX = x - legendSpacing- (legendSpacing/4);
157 | var boxY = y - legendSpacing/2;
158 |
159 | var box = raphael.rect(boxX, boxY, legendSpacing, legendSpacing)
160 | .attr('fill', colors[i])
161 | .attr('stroke-width', '0');
162 |
163 | var angle = types[idx] / totalLabors * 360;
164 | if (angle == 360) {
165 | var circle = raphael.circle(pieX, pieY, pieR)
166 | .attr({
167 | 'fill': colors[i],
168 | 'stroke': 'none'
169 | });
170 | } else {
171 | var pie = sector(
172 | pieX, pieY, pieR, lastAngle, lastAngle + angle,
173 | {
174 | 'fill': colors[i],
175 | 'stroke': 'none'
176 | }
177 | );
178 | lastAngle += angle;
179 | }
180 | i++;
181 | }
182 | }, 0);
183 | };
184 |
185 | function sector(cx, cy, r, startAngle, endAngle, params) {
186 | var x1 = cx + r * Math.cos(-startAngle * rad),
187 | x2 = cx + r * Math.cos(-endAngle * rad),
188 | y1 = cy + r * Math.sin(-startAngle * rad),
189 | y2 = cy + r * Math.sin(-endAngle * rad);
190 | return raphael.path(["M", cx, cy, "L", x1, y1, "A", r, r, 0, +(endAngle - startAngle > 180), 0, x2, y2, "z"]).attr(params);
191 | }
192 | }
193 | }
194 | }
195 |
196 | angular.module('hermesApp').directive('questProgressChart', questProgressChart);
197 | questProgressChart.$inject = ['HermesService', '$timeout', '$window'];
198 | })();
--------------------------------------------------------------------------------
/hermes/webapp/src/js/directives/scrollWatch.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | function scrollWatch ($anchorScroll, $timeout) {
3 | return {
4 | restrict: 'A',
5 | transclude: true,
6 | link: function ($scope, $ele, $attrs) {
7 | if ($scope.$last === true) {
8 | $timeout(function () {
9 | $anchorScroll();
10 | }, 100);
11 | }
12 | }
13 | }
14 | }
15 |
16 | angular.module('hermesApp').directive('scrollWatch', scrollWatch);
17 | scrollWatch.inject = ['$anchorScroll', '$timeout'];
18 | })();
--------------------------------------------------------------------------------
/hermes/webapp/src/js/directives/viewportHeight.js:
--------------------------------------------------------------------------------
1 | /**
2 | * directive for having a div's height based on the size of the viewport
3 | */
4 | (function() {
5 | function viewportHeight ($window) {
6 | return {
7 | restrict: 'A',
8 | scope: {
9 | },
10 | link: function ($scope, $ele, $attrs) {
11 | var minHeight = $attrs.minHeight || 200;
12 | var padding = $attrs.padding || 120;
13 |
14 | $ele.css('overflow', 'scroll');
15 |
16 | angular.element($window).bind('resize', function() {
17 | fixHeight();
18 | });
19 |
20 | function fixHeight() {
21 | var innerHeight = $window.innerHeight;
22 | var height = minHeight;
23 | if (innerHeight > minHeight) {
24 | height = innerHeight - padding - 80;
25 | }
26 |
27 | $ele.css('height', height + 'px');
28 | }
29 |
30 | fixHeight();
31 | }
32 | }
33 | }
34 |
35 | angular.module('hermesApp').directive('viewportHeight', viewportHeight);
36 | viewportHeight.$inject = ['$window'];
37 | })();
--------------------------------------------------------------------------------
/hermes/webapp/src/js/filters/encode.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | 'use strict';
4 |
5 | function EncodeFilter() {
6 | return window.encodeURIComponent;
7 | }
8 |
9 | angular.module('hermesApp').filter('encode', EncodeFilter);
10 | })();
--------------------------------------------------------------------------------
/hermes/webapp/src/js/filters/num.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | 'use strict';
4 |
5 | function NumFilter() {
6 | return function (input) {
7 | return parseInt(input, 10);
8 | };
9 | }
10 |
11 | angular.module('hermesApp').filter('num', NumFilter);
12 |
13 | })();
--------------------------------------------------------------------------------
/hermes/webapp/src/js/hermesApp.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | 'use strict';
4 |
5 | var app = angular.module('hermesApp',
6 | [
7 | 'ngAnimate',
8 | 'ngRoute',
9 | 'ngLocationUpdate',
10 | 'smoothScroll',
11 | 'ui.bootstrap',
12 | 'ngCookies',
13 | ]);
14 |
15 | app.config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) {
16 | $routeProvider.when('/v1/quests/:questId?', {
17 | templateUrl: '/templates/questStatus.html',
18 | reloadOnSearch: false
19 | }).when('/v1/quest/new', {
20 | templateUrl: '/templates/questCreation.html',
21 | reloadOnSearch: false
22 | }).when('/v1/quests/:questId/edit', {
23 | templateUrl: '/templates/questEdit.html',
24 | reloadOnSearch: false
25 | }).when('/v1/labors/:laborId?', {
26 | templateUrl: '/templates/laborList.html',
27 | reloadOnSearch: false
28 | }).when('/v1/fates', {
29 | templateUrl: '/templates/fateViewer.html',
30 | reloadOnSearch: false
31 | }).when('/home', {
32 | templateUrl: '/templates/userHome.html',
33 | reloadOnSearch: false
34 | }).otherwise({redirectTo: '/home/'});
35 |
36 | // use the HTML5 History API
37 | $locationProvider.html5Mode({
38 | enabled: true,
39 | requireBase: false
40 | });
41 | }]);
42 |
43 | })();
--------------------------------------------------------------------------------
/hermes/webapp/src/js/services/skipReload.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | angular.module('hermesApp').factory('skipReload', [
4 | '$route',
5 | '$rootScope',
6 | function ($route, $rootScope) {
7 | return function () {
8 | var lastRoute = $route.current;
9 | var un = $rootScope.$on('$locationChangeSuccess', function () {
10 | $route.current = lastRoute;
11 | un();
12 | });
13 | };
14 | }
15 | ]);
16 | })();
--------------------------------------------------------------------------------
/hermes/webapp/src/templates/fateViewer.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/hermes/webapp/src/templates/questCreation.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
For {{qc.hostList.length}} hosts:
12 |
13 |
14 |
16 |
17 | {{host}}
18 |
19 |
20 |
23 |
Add host:
24 |
25 |
26 |
29 |
30 |
31 | Add
32 |
33 |
34 |
35 |
36 |
Look up hosts by query:
37 |
{{qc.queryErrorMessage}}
38 |
39 |
40 |
43 |
44 |
45 | Go
46 |
47 |
48 |
49 |
50 |
51 |
53 |
54 | {{host}}
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | Add All {{qc.queriedHosts.length}} Hosts
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
Quest type:
71 |
74 |
75 |
76 |
77 |
78 |
79 |
Need help picking a quest type?
80 |
Use the explorer to see the quest types and what kind of workflow each implies..
81 |
Quest Type Explorer
82 |
83 |
84 |
85 |
86 |
87 |
88 |
Target Date:
89 |
90 |
91 |
Target Time:
92 |
93 |
94 |
95 |
96 |
The targeted completion date and time for the quest. As the date gets closer, notifications to service owners will intensify.
97 |
98 |
99 |
100 |
101 |
102 |
With description:
103 |
106 |
107 |
108 |
109 |
The Quest description is what most users will see so be as
110 | descriptive as possible. Let users know what is going on
111 | and what action they should take.
112 |
113 |
114 |
115 |
116 |
117 |
{{msg}}
118 |
119 |
120 |
121 | Create Quest
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 | Close
144 |
145 |
146 |
147 |
148 |
149 |
{{qc.successMessage}}
150 |
151 | Close
152 |
153 |
154 |
155 |
156 |
--------------------------------------------------------------------------------
/hermes/webapp/src/templates/questEdit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 | Creator/Owner:
13 | {{qc.quest.creator}}
14 |
17 |
19 |
20 |
22 |
23 |
25 |
26 |
27 |
28 |
29 |
The creator/owner of the quest is responsible for managing and closing out the quest.
30 |
31 |
32 |
33 |
34 |
35 |
36 | Current Target Time: {{qc.quest.targetTime}}
37 |
40 |
41 |
42 |
43 |
New Target Date:
44 |
45 |
46 |
New Target Time:
47 |
48 |
49 |
50 |
52 |
53 |
55 |
56 |
57 |
58 |
59 |
60 |
The targeted completion date and time for the quest. As the date gets closer, notifications to service owners will intensify.
61 |
62 |
63 |
64 |
65 |
66 |
67 |
Edit description:
68 |
70 |
71 |
{{qc.quest.description}}
72 |
73 |
75 |
76 |
77 |
79 |
80 |
82 |
83 |
84 |
85 |
86 |
87 |
The Quest description is what most users will see so be as
88 | descriptive as possible. Let users know what is going on
89 | and what action they should take.
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
{{qc.successMessage}}
105 |
106 | Close
107 |
108 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/hermes/webapp/src/templates/userHome.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Hello {{ctrl.user}}
4 |
5 |
6 |
7 |
{{ctrl.totalQuests}} total
9 | open quests. {{ctrl.totalUserCreatedQuests}} created by
10 | you.
11 |
12 |
13 | Loading
14 | Info
15 |
16 |
17 |
18 |
{{ctrl.totalLabors}} total
20 | open labors. {{ctrl.totalUserLabors}}
21 | {{ctrl.totalUserLabors == 1 ? "is" : "are"}} for you.
22 |
23 |
24 | Loading
25 | Info
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "bower": "^1.5.2",
4 | "del": "^2.0.0",
5 | "gulp": "^3.9.0",
6 | "gulp-bower": "0.0.10",
7 | "gulp-concat": "^2.6.0",
8 | "gulp-csslint": "^0.2.0",
9 | "gulp-jshint": "^1.11.2",
10 | "gulp-less": "^3.0.3",
11 | "gulp-minify-css": "^1.2.1",
12 | "gulp-ng-annotate": "^1.1.0",
13 | "gulp-rename": "^1.2.2",
14 | "gulp-sort": "^1.1.1",
15 | "gulp-uglify": "^1.4.0",
16 | "gulp-watch": "^4.3.5",
17 | "main-bower-files": "^2.9.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | -r requirements.txt
2 | mrproxy==0.3.3
3 | py==1.4.26
4 | pytest==2.6.4
5 | Pygments==2.0.1
6 | Sphinx==1.2.3
7 | argh==0.26.1
8 | docutils==0.12
9 | livereload==2.3.2
10 | pathtools==0.1.2
11 | sphinx-autobuild==0.4.0
12 | watchdog==0.8.2
13 | sphinxcontrib-httpdomain==1.3.0
14 | sphinx-rtd-theme==0.1.6
15 | pytest-capturelog==0.7
16 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | enum34==1.0.4
2 | cffi==0.9.2
3 | pyasn1==0.1.7
4 | six==1.9.0
5 | pycparser==2.10
6 | cryptography==0.8.1
7 | Jinja2==2.7.3
8 | Mako==1.0.1
9 | MarkupSafe==0.23
10 | PyYAML==3.11
11 | SQLAlchemy==0.9.8
12 | Werkzeug==0.9.6
13 | alembic==0.7.4
14 | argparse==1.4.0
15 | backports.ssl-match-hostname==3.4.0.2
16 | bittle==0.2.1
17 | certifi==14.05.14
18 | ipaddress==1.0.7
19 | tornado==4.0.2
20 | MySQL-python==1.2.5
21 | requests[security]==2.7.0
22 | python-dateutil==2.4.2
23 | annex==0.3.1
24 | pytz==2015.6
25 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 |
5 | from setuptools import find_packages
6 | from distutils.core import setup
7 |
8 | execfile('hermes/version.py')
9 |
10 | with open('requirements.txt') as requirements:
11 | required = requirements.read().splitlines()
12 |
13 | package_data = {}
14 | def get_package_data(package, base_dir):
15 | for dirpath, dirnames, filenames in os.walk(base_dir):
16 | dirpath = dirpath[len(package)+1:] # Strip package dir
17 | for filename in filenames:
18 | package_data.setdefault(package, []).append(os.path.join(dirpath, filename))
19 | for dirname in dirnames:
20 | get_package_data(package, dirname)
21 |
22 | get_package_data("hermes", "hermes/webapp/build")
23 | get_package_data("hermes", "hermes/templates")
24 |
25 | kwargs = {
26 | "name": "hermes",
27 | "version": str(__version__),
28 | "packages": find_packages(exclude=['tests']),
29 | "package_data": package_data,
30 | "scripts": ["bin/hermes-server", "bin/hermes", "bin/hermes-notify"],
31 | "description": "Hermes Event Management and Autotasker",
32 | "author": "Digant C Kasundra",
33 | "maintainer": "Digant C Kasundra",
34 | "author_email": "digant@dropbox.com",
35 | "maintainer_email": "digant@dropbox.com",
36 | "license": "Apache",
37 | "install_requires": required,
38 | "url": "https://github.com/dropbox/hermes",
39 | "download_url": "https://github.com/dropbox/hermes/archive/master.tar.gz",
40 | "classifiers": [
41 | "Programming Language :: Python",
42 | "Topic :: Software Development",
43 | "Topic :: Software Development :: Libraries",
44 | "Topic :: Software Development :: Libraries :: Python Modules",
45 | ]
46 | }
47 |
48 | setup(**kwargs)
49 |
--------------------------------------------------------------------------------
/tests/api_tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dropbox/hermes/36e5c0532571eed42d5f6edea35f755b837a5b2d/tests/api_tests/__init__.py
--------------------------------------------------------------------------------
/tests/api_tests/data/set1/event.json:
--------------------------------------------------------------------------------
1 | {
2 | "event1": {
3 | "hostname": "example",
4 | "user": "system",
5 | "eventTypeId": 1,
6 | "note": "example needs a reboot"
7 | },
8 | "event2": {
9 | "hostname": "example",
10 | "user": "system",
11 | "eventTypeId": 2,
12 | "note": "example needs a rebooted"
13 | },
14 | "event3": {
15 | "hostname": "example",
16 | "user": "system",
17 | "eventTypeId": 3,
18 | "note": "example needs a reboot"
19 | },
20 | "event4": {
21 | "hostname": "example",
22 | "user": "system",
23 | "eventTypeId": 4,
24 | "note": "example needs a rebooted"
25 | },
26 | "event5": {
27 | "hostname": "example",
28 | "user": "system",
29 | "eventTypeId": 5,
30 | "note": "example needs a reboot"
31 | },
32 | "event6": {
33 | "hostname": "example",
34 | "user": "system",
35 | "eventTypeId": 3,
36 | "note": "example needs a rebooted"
37 | },
38 | "event7": {
39 | "hostname": "example",
40 | "user": "system",
41 | "eventTypeId": 4,
42 | "note": "example needs a reboot"
43 | },
44 | "event8": {
45 | "hostname": "example",
46 | "user": "system",
47 | "eventTypeId": 1,
48 | "note": "example needs a rebooted"
49 | },
50 | "event9": {
51 | "hostname": "sample",
52 | "user": "system",
53 | "eventTypeId": 3,
54 | "note": "sample needs a reboot"
55 | },
56 | "event10": {
57 | "hostname": "sample",
58 | "user": "system",
59 | "eventTypeId": 4,
60 | "note": "sample needs a rebooted"
61 | },
62 | "event11": {
63 | "hostname": "sample",
64 | "user": "system",
65 | "eventTypeId": 5,
66 | "note": "sample needs a reboot"
67 | },
68 | "event12": {
69 | "hostname": "test",
70 | "user": "system",
71 | "eventTypeId": 3,
72 | "note": "test needs a reboot"
73 | },
74 | "event13": {
75 | "hostname": "test",
76 | "user": "system",
77 | "eventTypeId": 1,
78 | "note": "test needs a reboot"
79 | },
80 | "event14": {
81 | "hostname": "test",
82 | "user": "system",
83 | "eventTypeId": 2,
84 | "note": "test needs a reboot"
85 | },
86 | "event15": {
87 | "hostname": "test",
88 | "user": "system",
89 | "eventTypeId": 4,
90 | "note": "test needs a reboot"
91 | }
92 | }
--------------------------------------------------------------------------------
/tests/api_tests/data/set1/eventtypes.json:
--------------------------------------------------------------------------------
1 | {
2 | "eventTypes": [
3 | {
4 | "category": "system-reboot",
5 | "state": "required",
6 | "description": "This system requires a reboot."
7 | },
8 | {
9 | "category": "system-reboot",
10 | "state": "completed",
11 | "description": "This system rebooted."
12 | },
13 | {
14 | "category": "system-maintenance",
15 | "state": "required",
16 | "description": "This system requires maintenance."
17 | },
18 | {
19 | "category": "system-maintenance",
20 | "state": "ready",
21 | "description": "This system is ready for maintenance."
22 | },
23 | {
24 | "category": "system-maintenance",
25 | "state": "completed",
26 | "description": "System maintenance completed."
27 | },
28 | {
29 | "category": "system-shutdown",
30 | "state": "required",
31 | "description": "System shutdown required."
32 | },
33 | {
34 | "category": "system-shutdown",
35 | "state": "completed",
36 | "description": "System shutdown completed."
37 | }
38 | ]
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/tests/api_tests/data/set1/fates.json:
--------------------------------------------------------------------------------
1 | {
2 | "fate1": {
3 | "creationEventTypeId": 1,
4 | "description": "A system that needs a reboot can be cleared by rebooting the machine."
5 | },
6 | "fate2": {
7 | "creationEventTypeId": 2,
8 | "followsId": 1,
9 | "description": "A system that needs a reboot can be cleared by rebooting the machine."
10 | },
11 | "fate3": {
12 | "creationEventTypeId": 3,
13 | "description": "A system that needs maintenance made ready before maintenance can occur."
14 | },
15 | "fate4": {
16 | "creationEventTypeId": 4,
17 | "followsId": 3,
18 | "forCreator": true,
19 | "forOwner": false,
20 | "description": "A system that needs maintenance made ready before maintenance can occur."
21 | },
22 | "fate5": {
23 | "creationEventTypeId": 5,
24 | "followsId": 4,
25 | "description": "Maintenance must be performed on a system that is prepped."
26 | },
27 | "fate6": {
28 | "creationEventTypeId": 3,
29 | "description": "Maintenance must be performed on a new system."
30 | },
31 | "fate7": {
32 | "creationEventTypeId": 5,
33 | "followsId": 6,
34 | "description": "Maintenance must be performed on a new system that is prepped."
35 | }
36 |
37 | }
--------------------------------------------------------------------------------
/tests/api_tests/data/set1/hosts.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosts": [
3 | {
4 | "hostname": "example"
5 | },
6 | {
7 | "hostname": "sample"
8 | },
9 | {
10 | "hostname": "test"
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/tests/api_tests/fixtures.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import json
3 | import logging
4 | import os
5 | import pytest
6 | import requests
7 | import socket
8 | import threading
9 | import tornado
10 | import tornado.httpserver
11 | import tornado.ioloop
12 | from tornado import netutil
13 |
14 | import hermes
15 | from hermes import models
16 | from hermes.models import Model, Session, Host, EventType, Event, Fate, Labor, Quest
17 | from hermes.settings import settings
18 | from hermes.app import Application
19 | from .util import load_json, Client
20 |
21 |
22 | sa_log = logging.getLogger("sqlalchemy.engine.base.Engine")
23 |
24 | # Uncomment to have all queries printed out
25 | # sa_log.setLevel(logging.INFO)
26 |
27 |
28 | class Server(object):
29 | """ Wrapper around Tornado server with test helpers. """
30 |
31 | def __init__(self, tornado_app):
32 | self.tornado_app = tornado_app
33 | self.server = tornado.httpserver.HTTPServer(
34 | tornado_app
35 | )
36 | self.server.add_sockets(netutil.bind_sockets(
37 | None, "localhost", family=socket.AF_INET
38 | ))
39 | self.server.start()
40 | self.io_thread = threading.Thread(
41 | target=tornado.ioloop.IOLoop.instance().start
42 | )
43 | self.io_thread.start()
44 |
45 | @property
46 | def port(self):
47 | return self.server._sockets.values()[0].getsockname()[1]
48 |
49 |
50 | @pytest.fixture()
51 | def tornado_app(request, tmpdir):
52 | db_path = tmpdir.join("nsot.sqlite")
53 | db_engine = models.get_db_engine("sqlite:///%s" % db_path)
54 |
55 | Model.metadata.drop_all(db_engine)
56 | Model.metadata.create_all(db_engine)
57 | Session.configure(bind=db_engine)
58 |
59 | Fate._all_fates = None
60 |
61 | my_settings = {
62 | "db_engine": db_engine,
63 | "db_session": Session,
64 | "domain": "example.com"
65 | }
66 |
67 | tornado_settings = {
68 | "debug": False,
69 | }
70 |
71 | return Application(my_settings=my_settings, **tornado_settings)
72 |
73 |
74 | @pytest.fixture()
75 | def tornado_server(request, tornado_app):
76 |
77 | server = Server(tornado_app)
78 |
79 | def fin():
80 | tornado.ioloop.IOLoop.instance().stop()
81 | server.io_thread.join()
82 | request.addfinalizer(fin)
83 |
84 | return server
85 |
86 |
87 | @pytest.fixture
88 | def session(request, tmpdir):
89 | db_path = tmpdir.join("nsot.sqlite")
90 | db_engine = models.get_db_engine("sqlite:///%s" % db_path)
91 |
92 | Model.metadata.drop_all(db_engine)
93 | Model.metadata.create_all(db_engine)
94 | Session.configure(bind=db_engine)
95 | session = Session()
96 |
97 | def fin():
98 | session.close()
99 | request.addfinalizer(fin)
100 |
101 | return session
102 |
103 | @pytest.fixture
104 | def sample_data1_server(tornado_server):
105 | client = Client(tornado_server)
106 | hosts_data = load_json("set1/hosts.json")
107 | client.create("/hosts/", hosts=hosts_data["hosts"])
108 |
109 | event_types_data = load_json("set1/eventtypes.json")
110 | client.create("/eventtypes/", eventTypes=event_types_data["eventTypes"])
111 |
112 | events = load_json("set1/event.json")
113 | client.post("/events/", json=events["event1"])
114 | client.post("/events/", json=events['event2'])
115 |
116 | fates = load_json("set1/fates.json")
117 | client.post("/fates/", json=fates["fate1"])
118 | client.post("/fates/", json=fates["fate2"])
119 | client.post("/fates/", json=fates["fate3"])
120 | client.post("/fates/", json=fates["fate4"])
121 | client.post("/fates/", json=fates["fate5"])
122 |
123 | return client
124 |
125 |
126 | @pytest.fixture
127 | def sample_data2_server(tornado_server):
128 | client = Client(tornado_server)
129 | hosts_data = load_json("set1/hosts.json")
130 | client.create("/hosts/", hosts=hosts_data["hosts"])
131 |
132 | event_types_data = load_json("set1/eventtypes.json")
133 | client.create("/eventtypes/", eventTypes=event_types_data["eventTypes"])
134 |
135 | events = load_json("set1/event.json")
136 | for x in range(1, len(events) + 1):
137 | client.post("/events/", json=events["event{}".format(x)])
138 |
139 | fates = load_json("set1/fates.json")
140 | client.post("/fates/", json=fates["fate1"])
141 | client.post("/fates/", json=fates["fate2"])
142 | client.post("/fates/", json=fates["fate3"])
143 | client.post("/fates/", json=fates["fate4"])
144 | client.post("/fates/", json=fates["fate5"])
145 |
146 | return client
--------------------------------------------------------------------------------
/tests/api_tests/test_eventtypes.py:
--------------------------------------------------------------------------------
1 | import json
2 | import pytest
3 | import requests
4 |
5 | from .fixtures import tornado_server, tornado_app, sample_data1_server
6 | from .util import (
7 | assert_error, assert_success, assert_created, assert_deleted, Client
8 | )
9 |
10 |
11 | def test_malformed(tornado_server):
12 | client = Client(tornado_server)
13 | assert_error(client.post("/eventtypes", data="Non-JSON"), 400)
14 |
15 |
16 | def test_creation(tornado_server):
17 | client = Client(tornado_server)
18 | assert_success(client.get("/eventtypes"), {
19 | "eventTypes": [],
20 | "limit": 10,
21 | "offset": 0,
22 | "totalEventTypes": 0,
23 | })
24 |
25 | assert_created(
26 | client.create(
27 | "/eventtypes",
28 | category="foo",
29 | state="bar",
30 | description="This is a test",
31 | ), "/api/v1/eventtypes/1"
32 | )
33 | assert_error(
34 | client.create(
35 | "/eventtypes",
36 | category="foo",
37 | state="bar",
38 | description="Reject duplicate"
39 | ), 409
40 | )
41 |
42 | assert_success(
43 | client.get("/eventtypes"),
44 | {
45 | "eventTypes": [{
46 | "id": 1,
47 | "category": "foo",
48 | "state": "bar",
49 | "description": "This is a test",
50 | "restricted": False,
51 | }],
52 | "limit": 10,
53 | "offset": 0,
54 | "totalEventTypes": 1,
55 | }
56 | )
57 |
58 | assert_success(
59 | client.get("/eventtypes/1"),
60 | {
61 | "id": 1,
62 | "category": "foo",
63 | "state": "bar",
64 | "description": "This is a test",
65 | "restricted": False,
66 | "events": [],
67 | "limit": 10,
68 | "offset": 0,
69 | }
70 | )
71 |
72 | assert_created(
73 | client.create(
74 | "/eventtypes",
75 | category="foo",
76 | state="baz",
77 | description="This is a second test"
78 | ), "/api/v1/eventtypes/2"
79 | )
80 | assert_success(
81 | client.get("/eventtypes?expand=fates",
82 | params={"category": "foo", "state": "baz"}),
83 | {
84 | "eventTypes": [{
85 | "id": 2,
86 | "category": "foo",
87 | "state": "baz",
88 | "description": "This is a second test",
89 | "restricted": False,
90 | "autoCreates": []
91 | }],
92 | "limit": 10,
93 | "offset": 0,
94 | "totalEventTypes": 1
95 | }
96 | )
97 |
98 |
99 | def test_create_multiple(tornado_server):
100 | client = Client(tornado_server)
101 | assert_success(client.get("/eventtypes"), {
102 | "eventTypes": [],
103 | "limit": 10,
104 | "offset": 0,
105 | "totalEventTypes": 0,
106 | })
107 |
108 | client.create(
109 | "/eventtypes",
110 | eventTypes=[
111 | {
112 | "category": "foo",
113 | "state": "bar",
114 | "description": "This is a test",
115 | "restricted": False,
116 | },
117 | {
118 | "category": "foo",
119 | "state": "baz",
120 | "description": "This is a 2nd test",
121 | "restricted": False,
122 | }
123 | ]
124 | )
125 |
126 | assert_success(client.get("/eventtypes"), {
127 | "limit": 10,
128 | "offset": 0,
129 | "totalEventTypes": 2,
130 | }, strip="eventTypes")
131 |
132 |
133 | def test_update(tornado_server):
134 | client = Client(tornado_server)
135 | assert_success(client.get("/eventtypes"), {
136 | "eventTypes": [],
137 | "limit": 10,
138 | "offset": 0,
139 | "totalEventTypes": 0,
140 | })
141 |
142 | assert_created(
143 | client.create(
144 | "/eventtypes",
145 | category="foo",
146 | state="bar",
147 | description="This is a test"
148 | ), "/api/v1/eventtypes/1"
149 | )
150 |
151 | assert_success(
152 | client.update("/eventtypes/1", description="new"),
153 | {
154 | "id": 1,
155 | "category": "foo",
156 | "state": "bar",
157 | "description": "new",
158 | "restricted": False,
159 | }
160 | )
161 |
162 | assert_error(client.update("/eventtypes/1"), 400)
163 |
164 |
165 | def test_filter_by_creating_types(sample_data1_server):
166 | client = sample_data1_server
167 |
168 | assert_success(
169 | client.get("/eventtypes?startingTypes=true"), {
170 | "limit": 10,
171 | "offset": 0,
172 | "totalEventTypes": 2
173 | }, strip=['eventTypes']
174 | )
175 |
--------------------------------------------------------------------------------
/tests/api_tests/test_fates.py:
--------------------------------------------------------------------------------
1 | import json
2 | import pytest
3 | import requests
4 |
5 | from .fixtures import tornado_server, tornado_app, sample_data1_server
6 | from .util import (
7 | assert_error, assert_success, assert_created, assert_deleted, Client
8 | )
9 |
10 |
11 | def test_malformed(sample_data1_server):
12 | client = sample_data1_server
13 | assert_error(client.post("/fates", data="Non-JSON"), 400)
14 |
15 |
16 | def test_bad_creation(sample_data1_server):
17 | """Fate's must be either set to forOwner, forCreator, or both"""
18 | client = sample_data1_server
19 | assert_error(
20 | client.post(
21 | "/fates",
22 | data={
23 | "creationEventTypeId": 6,
24 | "forOwner": False,
25 | "forCreator": False,
26 | "precedesIds": [],
27 | "description":"New fate"
28 | }
29 | ),
30 | 400
31 | )
32 |
33 |
34 | def test_creation(sample_data1_server):
35 | client = sample_data1_server
36 | assert_success(
37 | client.get("/eventtypes"),
38 | {
39 | "limit": 10,
40 | "offset": 0,
41 | "totalEventTypes": 7,
42 | },
43 | strip="eventTypes"
44 | )
45 |
46 | assert_created(
47 | client.create(
48 | "/fates/",
49 | creationEventTypeId=6,
50 | description="New fate"
51 | ),
52 | "/api/v1/fates/6"
53 | )
54 |
55 | assert_created(
56 | client.create(
57 | "/fates/",
58 | creationEventTypeId=7,
59 | followsId=6,
60 | description="New fate2"
61 | ),
62 | "/api/v1/fates/7"
63 | )
64 |
65 | assert_success(
66 | client.get("/fates/6"),
67 | {
68 | "id": 6,
69 | "creationEventTypeId": 6,
70 | "followsId": None,
71 | "forOwner": True,
72 | "forCreator": False,
73 | "precedesIds": [7],
74 | "description": "New fate"
75 | }
76 | )
77 |
78 | assert_success(
79 | client.get("/fates/7"),
80 | {
81 | "id": 7,
82 | "creationEventTypeId": 7,
83 | "followsId": 6,
84 | "forOwner": True,
85 | "forCreator": False,
86 | "precedesIds": [],
87 | "description": "New fate2"
88 | }
89 | )
90 |
91 |
92 | def test_update(sample_data1_server):
93 | client = sample_data1_server
94 | assert_created(
95 | client.create(
96 | "/fates/",
97 | creationEventTypeId=6,
98 | description="New fate"
99 | ),
100 | "/api/v1/fates/6"
101 | )
102 |
103 | assert_success(
104 | client.get("/fates/6"),
105 | {
106 | "id": 6,
107 | "creationEventTypeId": 6,
108 | "followsId": None,
109 | "forOwner": True,
110 | "forCreator": False,
111 | "precedesIds": [],
112 | "description": "New fate"
113 | }
114 | )
115 |
116 | assert_success(
117 | client.update(
118 | "/fates/6",
119 | followsId=1
120 | ),
121 | {
122 | "id": 6,
123 | "creationEventTypeId": 6,
124 | "followsId": 1,
125 | "forOwner": True,
126 | "forCreator": False,
127 | "precedesIds": [],
128 | "description": "New fate"
129 | }
130 | )
131 |
132 | assert_success(
133 | client.update(
134 | "/fates/6",
135 | description="New desc"
136 | ),
137 | {
138 | "id": 6,
139 | "creationEventTypeId": 6,
140 | "followsId": 1,
141 | "forOwner": True,
142 | "forCreator": False,
143 | "precedesIds": [],
144 | "description": "New desc"
145 | }
146 | )
147 |
148 | assert_success(
149 | client.update(
150 | "/fates/6",
151 | followsId=None,
152 | description="Another desc"
153 | ),
154 | {
155 | "id": 6,
156 | "creationEventTypeId": 6,
157 | "followsId": None,
158 | "forOwner": True,
159 | "forCreator": False,
160 | "precedesIds": [],
161 | "description": "Another desc"
162 | }
163 | )
164 |
165 | assert_success(
166 | client.get("/fates/6"),
167 | {
168 | "id": 6,
169 | "creationEventTypeId": 6,
170 | "followsId": None,
171 | "forOwner": True,
172 | "forCreator": False,
173 | "precedesIds": [],
174 | "description": "Another desc"
175 | }
176 | )
177 |
178 |
--------------------------------------------------------------------------------
/tests/api_tests/test_hosts.py:
--------------------------------------------------------------------------------
1 | import json
2 | import pytest
3 | import requests
4 |
5 | from .fixtures import tornado_server, tornado_app
6 | from .util import (
7 | assert_error, assert_success, assert_created, assert_deleted, Client
8 | )
9 |
10 |
11 | def test_malformed(tornado_server):
12 | client = Client(tornado_server)
13 | assert_error(client.post("/hosts", data="Non-JSON"), 400)
14 |
15 |
16 | def test_creation(tornado_server):
17 | client = Client(tornado_server)
18 | assert_success(client.get("/hosts"), {
19 | "hosts": [],
20 | "limit": 10,
21 | "offset": 0,
22 | "totalHosts": 0,
23 | })
24 |
25 | assert_created(
26 | client.create("/hosts", hostname="example"), "/api/v1/hosts/example"
27 | )
28 | assert_error(client.create("/hosts", hostname="example"), 409)
29 |
30 | assert_success(
31 | client.get("/hosts"),
32 | {
33 | "hosts": [{
34 | "id": 1,
35 | "hostname": "example"
36 | }],
37 | "limit": 10,
38 | "offset": 0,
39 | "totalHosts": 1,
40 | }
41 | )
42 |
43 | assert_success(
44 | client.get("/hosts/example"),
45 | {
46 | "id": 1,
47 | "hostname": "example",
48 | "events": [],
49 | "labors": [],
50 | "quests": [],
51 | "lastEvent": None,
52 | "limit": 10,
53 | "offset": 0,
54 | }
55 | )
56 |
57 | assert_created(client.create("/hosts", hostname="sample"), "/api/v1/hosts/sample")
58 | assert_success(
59 | client.get("/hosts", params={"hostname": "sample"}),
60 | {
61 | "hosts": [{
62 | "id": 2,
63 | "hostname": "sample"
64 | }],
65 | "limit": 10,
66 | "offset": 0,
67 | "totalHosts": 1
68 | }
69 | )
70 |
71 | def test_create_multiple(tornado_server):
72 | client = Client(tornado_server)
73 | assert_success(client.get("/hosts"), {
74 | "hosts": [],
75 | "limit": 10,
76 | "offset": 0,
77 | "totalHosts": 0,
78 | })
79 |
80 | client.create(
81 | "/hosts",
82 | hosts=[
83 | {"hostname":"example"},
84 | {"hostname":"sample"},
85 | {"hostname":"test"}
86 | ]
87 | )
88 |
89 | assert_success(client.get("/hosts"), {
90 | "limit": 10,
91 | "offset": 0,
92 | "totalHosts": 3,
93 | }, strip="hosts")
94 |
95 |
96 | def test_update(tornado_server):
97 | client = Client(tornado_server)
98 |
99 | client.create("/hosts", hostname="testname")
100 |
101 | assert_success(
102 | client.update("/hosts/testname", hostname="newname"),
103 | {
104 | "id": 1,
105 | "hostname": "newname"
106 | }
107 | )
108 |
109 | # test failure of empty update calls
110 | assert_error(client.update("/hosts/newname"), 400)
111 |
112 |
113 | def test_merging(tornado_server):
114 | """When renaming a server to an existing servername, just merge them"""
115 | client = Client(tornado_server)
116 |
117 | client.create("/hosts", hostname="testname")
118 | client.create("/hosts", hostname="newname")
119 |
120 | assert_success(
121 | client.update("/hosts/testname", hostname="newname"),
122 | {
123 | "id": 2,
124 | "hostname": "newname"
125 | }
126 | )
--------------------------------------------------------------------------------
/tests/api_tests/test_labors.py:
--------------------------------------------------------------------------------
1 | import json
2 | import pytest
3 | import requests
4 |
5 | from datetime import datetime, timedelta
6 |
7 | from .fixtures import tornado_server, tornado_app, sample_data1_server
8 | from .util import (
9 | assert_error, assert_success, assert_created, assert_deleted, Client
10 | )
11 |
12 |
13 | def test_malformed(sample_data1_server):
14 | client = sample_data1_server
15 | assert_error(client.post("/quests", data="Non-JSON"), 400)
16 |
17 |
18 | def test_creation(sample_data1_server):
19 | client = sample_data1_server
20 | assert_success(
21 | client.get("/events"),
22 | {
23 | "limit": 10,
24 | "offset": 0,
25 | "totalEvents": 2
26 | },
27 | strip=["timestamp", "events"]
28 | )
29 |
30 | assert_success(
31 | client.get("/quests"),
32 | {
33 | "limit": 10,
34 | "offset": 0,
35 | "totalQuests": 0,
36 | "quests": []
37 | }
38 | )
39 |
40 | assert_success(
41 | client.get("/labors"),
42 | {
43 | "limit": 10,
44 | "offset": 0,
45 | "totalLabors": 0,
46 | "labors": []
47 | }
48 | )
49 |
50 | target_time = datetime.utcnow() + timedelta(days=7)
51 |
52 | assert_created(
53 | client.create(
54 | "/quests",
55 | creator="johnny",
56 | fateId=1,
57 | targetTime=str(target_time),
58 | description="This is a quest almighty",
59 | hostnames=["example", "sample", "test"]
60 | ),
61 | "/api/v1/quests/1"
62 | )
63 |
64 | assert_success(
65 | client.get("/labors"),
66 | {
67 | "limit": 10,
68 | "offset": 0,
69 | "totalLabors": 3,
70 | "labors": [{"ackTime": None,
71 | "ackUser": None,
72 | "fateId": 1,
73 | "closingFateId": None,
74 | "completionEventId": None,
75 | "creationEventId": 3,
76 | "targetTime": str(target_time),
77 | "hostId": 1,
78 | "forOwner": True,
79 | "forCreator": False,
80 | "id": 1,
81 | "startingLaborId": None,
82 | "questId": 1},
83 | {"ackTime": None,
84 | "ackUser": None,
85 | "completionEventId": None,
86 | "creationEventId": 4,
87 | "targetTime": str(target_time),
88 | "hostId": 2,
89 | "forOwner": True,
90 | "forCreator": False,
91 | "fateId": 1,
92 | "closingFateId": None,
93 | "id": 2,
94 | "startingLaborId": None,
95 | "questId": 1},
96 | {"ackTime": None,
97 | "ackUser": None,
98 | "completionEventId": None,
99 | "creationEventId": 5,
100 | "targetTime": str(target_time),
101 | "hostId": 3,
102 | "forOwner": True,
103 | "forCreator": False,
104 | "fateId": 1,
105 | "closingFateId": None,
106 | "id": 3,
107 | "startingLaborId": None,
108 | "questId": 1}],
109 | },
110 | strip=["creationTime", "completionTime"]
111 | )
112 |
113 |
114 | def test_update(sample_data1_server):
115 | client = sample_data1_server
116 |
117 | # create a quest without a target_time
118 | assert_created(
119 | client.create(
120 | "/quests",
121 | creator="johnny",
122 | fateId=1,
123 | description="This is a quest almighty",
124 | hostnames=["example", "sample", "test"]
125 | ),
126 | "/api/v1/quests/1"
127 | )
128 |
129 | # make sure 3 labors was created for this quest
130 | assert_success(
131 | client.get("/labors"),
132 | {
133 | "limit": 10,
134 | "offset": 0,
135 | "totalLabors": 3
136 | },
137 | strip=["creationTime", "labors"]
138 | )
139 |
140 | # create a new event that would create another labor
141 | assert_created(
142 | client.create(
143 | "/events",
144 | hostname="example",
145 | user="testman@example.com",
146 | eventTypeId=1,
147 | note="This is a test event"
148 | ),
149 | "/api/v1/events/6"
150 | )
151 |
152 | # make sure the labor is not attached to a quest
153 | assert_success(
154 | client.get("/labors/4"),
155 | {
156 | "ackTime": None,
157 | "ackUser": None,
158 | "completionEventId": None,
159 | "completionTime": None,
160 | "creationEventId": 6,
161 | "hostId": 1,
162 | "forOwner": True,
163 | "forCreator": False,
164 | "fateId": 1,
165 | "closingFateId": None,
166 | "id": 4,
167 | "startingLaborId": None,
168 | "questId": None
169 | },
170 | strip=["creationTime"]
171 | )
172 |
173 | # attach the labor to a quest
174 | response = client.update(
175 | "/labors/4",
176 | ackUser="johnny@example.com",
177 | questId=1
178 | )
179 |
180 | # make sure the labor is attached to the quest
181 | assert_success(
182 | response,
183 | {
184 | "ackUser": "johnny@example.com",
185 | "completionEventId": None,
186 | "completionTime": None,
187 | "creationEventId": 6,
188 | "targetTime": None,
189 | "hostId": 1,
190 | "fateId": 1,
191 | "closingFateId": None,
192 | "forOwner": True,
193 | "forCreator": False,
194 | "id": 4,
195 | "startingLaborId": None,
196 | "questId": 1
197 | },
198 | strip=["creationTime", "ackTime"]
199 | )
200 |
201 | assert response.json()['ackTime'] is not None
202 |
203 |
204 | def test_labor_filter_by_eventttype(sample_data1_server):
205 | client = sample_data1_server
206 |
207 | assert_success(
208 | client.get("/labors"),
209 | {
210 | "limit": 10,
211 | "offset": 0,
212 | "totalLabors": 0,
213 | "labors": []
214 | }
215 | )
216 |
217 | # create a quest without a target_time
218 | assert_created(
219 | client.create(
220 | "/quests",
221 | creator="johnny",
222 | fateId=1,
223 | description="This is a quest almighty",
224 | hostnames=["example", "sample", "test"]
225 | ),
226 | "/api/v1/quests/1"
227 | )
228 |
229 | # create a quest without a target_time
230 | assert_created(
231 | client.create(
232 | "/quests",
233 | creator="johnny",
234 | fateId=3,
235 | description="This is a 2nd quest almighty",
236 | hostnames=["example", "sample", "test"]
237 | ),
238 | "/api/v1/quests/2"
239 | )
240 |
241 | assert_success(
242 | client.get("/labors"),
243 | {
244 | "limit": 10,
245 | "offset": 0,
246 | "totalLabors": 6,
247 | },
248 | strip=["labors"]
249 | )
250 |
251 | assert_success(
252 | client.get("/labors?hostname=example"),
253 | {
254 | "limit": 10,
255 | "offset": 0,
256 | "totalLabors": 2
257 | },
258 | strip=["labors"]
259 | )
260 |
261 | assert_success(
262 | client.get("/labors?category=system-reboot&state=required"),
263 | {
264 | "limit": 10,
265 | "offset": 0,
266 | "totalLabors": 3
267 | },
268 | strip=["labors"]
269 | )
270 |
271 | assert_success(
272 | client.get("/labors?category=system-maintenance"),
273 | {
274 | "limit": 10,
275 | "offset": 0,
276 | "totalLabors": 3
277 | },
278 | strip=["labors"]
279 | )
280 |
281 |
282 | def test_quest_expansion(sample_data1_server):
283 | client = sample_data1_server
284 |
285 | # create a quest without a target_time
286 | assert_created(
287 | client.create(
288 | "/quests",
289 | creator="johnny",
290 | fateId=1,
291 | description="This is a quest almighty",
292 | hostnames=["example"]
293 | ),
294 | "/api/v1/quests/1"
295 | )
296 |
297 | assert_created(
298 | client.create(
299 | "/events",
300 | eventTypeId=1,
301 | hostname="sample",
302 | user="testman@example.com",
303 | ),
304 | "/api/v1/events/4"
305 | )
306 |
307 | assert_success(
308 | client.get("/labors?expand=quests"),
309 | {
310 | "limit": 10,
311 | "offset": 0,
312 | "totalLabors": 2,
313 | "labors": [
314 | {'ackTime': None,
315 | 'ackUser': None,
316 | 'completionEventId': None,
317 | 'completionTime': None,
318 | 'creationEventId': 3,
319 | 'forCreator': False,
320 | 'forOwner': True,
321 | 'hostId': 1,
322 | 'id': 1,
323 | 'fateId': 1,
324 | "closingFateId": None,
325 | 'quest': {
326 | 'completionTime': None,
327 | 'creator': 'johnny@example.com',
328 | 'description': 'This is a quest almighty',
329 | 'id': 1,
330 | 'targetTime': None
331 | },
332 | 'questId': 1,
333 | 'startingLaborId': None,
334 | 'targetTime': None
335 | },
336 | {'ackTime': None,
337 | 'ackUser': None,
338 | 'completionEventId': None,
339 | 'completionTime': None,
340 | 'creationEventId': 4,
341 | 'forCreator': False,
342 | 'forOwner': True,
343 | 'hostId': 2,
344 | 'id': 2,
345 | 'fateId': 1,
346 | "closingFateId": None,
347 | 'quest': None,
348 | 'questId': None,
349 | 'startingLaborId': None
350 | }
351 | ]
352 | },
353 | strip=["embarkTime", "creationTime"]
354 | )
--------------------------------------------------------------------------------
/tests/api_tests/util.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import requests
4 | import urllib
5 | import urlparse
6 |
7 |
8 | def _deep_sort(obj):
9 | if isinstance(obj, dict):
10 | return {
11 | key: _deep_sort(value)
12 | for key, value in obj.iteritems()
13 | }
14 | elif isinstance(obj, list):
15 | return sorted(_deep_sort(elem) for elem in obj)
16 | return obj
17 |
18 |
19 | def _stripper(obj, strip):
20 | if isinstance(obj, dict):
21 | json = {}
22 | for key, value in obj.iteritems():
23 | if isinstance(value, basestring):
24 | value = str(value)
25 | if key not in strip:
26 | json[str(key)] = _stripper(value, strip)
27 |
28 | return json
29 | elif isinstance(obj, list):
30 | return [_stripper(elem, strip) for elem in obj]
31 | return obj
32 |
33 |
34 | def assert_error(response, code):
35 | output = response.json()
36 | assert output["status"] == "error"
37 | assert output["error"]["code"] == code
38 |
39 |
40 | def assert_success(response, data=None, ignore_order=True, strip=[]):
41 | output = response.json()
42 | if isinstance(strip, basestring):
43 | strip = [strip, "href"]
44 | else:
45 | strip.append("href")
46 | output = _stripper(output, strip)
47 | assert response.status_code == 200
48 | assert output["status"] == "ok"
49 |
50 | data["status"] = "ok"
51 | if ignore_order:
52 | assert _deep_sort(output) == _deep_sort(data)
53 | else:
54 | assert output == data
55 |
56 |
57 | def assert_created(response, location, data=None):
58 | output = response.json()
59 | assert response.status_code == 201
60 | assert output["status"] == "created"
61 | assert response.headers.get("Location") == location
62 | if data is not None:
63 | assert output == data
64 |
65 |
66 | def assert_deleted(response):
67 | output = response.json()
68 | assert response.status_code == 200
69 | assert output["status"] == "ok"
70 |
71 |
72 | class Client(object):
73 | def __init__(self, tornado_server, user="user"):
74 | self.tornado_server = tornado_server
75 | self.user = "{}@localhost".format(user)
76 |
77 | @property
78 | def base_url(self):
79 | return "http://localhost:{}/api/v1".format(self.tornado_server.port)
80 |
81 | def request(self, method, url, **kwargs):
82 |
83 | headers = {
84 | "X-NSoT-Email": self.user
85 | }
86 |
87 | if method.lower() in ("put", "post"):
88 | headers["Content-type"] = "application/json"
89 |
90 | return requests.request(
91 | method, self.base_url + url,
92 | headers=headers, **kwargs
93 | )
94 |
95 | def get(self, url, **kwargs):
96 | return self.request("GET", url, **kwargs)
97 |
98 | def post(self, url, **kwargs):
99 | return self.request("POST", url, **kwargs)
100 |
101 | def put(self, url, **kwargs):
102 | return self.request("PUT", url, **kwargs)
103 |
104 | def delete(self, url, **kwargs):
105 | return self.request("DELETE", url, **kwargs)
106 |
107 | def create(self, url, **kwargs):
108 | return self.post(url, data=json.dumps(kwargs))
109 |
110 | def update(self, url, **kwargs):
111 | return self.put(url, data=json.dumps(kwargs))
112 |
113 |
114 | def load_json(relpath):
115 | """
116 | Load JSON files relative to this directory.
117 |
118 | Files are loaded from the 'data' directory. So for example for
119 | ``/path/to/data/devices/foo.json`` the ``relpath`` would be
120 | ``devices/foo.json``.
121 |
122 | :param relpath:
123 | Relative path to our directory's "data" dir
124 | """
125 | our_path = os.path.dirname(os.path.abspath(__file__))
126 | data_dir = os.path.join(our_path, 'data')
127 | filepath = os.path.join(data_dir, relpath)
128 | with open(filepath, 'rb') as f:
129 | return json.load(f)
130 |
131 |
132 | def run_set_queries(resource_name, client, device_queries):
133 | """
134 | Run set queries on the specified resource.
135 |
136 | The directory structure is expected to match the resource path. So
137 | "devices/query" would map to both the data directory for JSON response
138 | files, and the API URL of "/api/sites/1/devices/query".
139 |
140 | The ``device_queries is expected to be a list of 2-tuples of (query,
141 | filename) where query is a set query and filename is a file containing the
142 | expected response JSON. Example::
143 |
144 | [
145 | ('foo=bar', 'test1.json'),
146 | ('bar=baz', 'test2.json'),
147 | ]
148 |
149 | :param resource_name:
150 | The resource for which to run query tests
151 |
152 | :param client:
153 | A ``Client`` instance
154 |
155 | :param devices_queries:
156 | A list of 2-tuples of (query, filename)
157 | """
158 | base_path = '/sites/1'
159 | path = os.path.join(resource_name, 'query')
160 | base_uri = os.path.join(base_path, path)
161 | uri = base_uri + '?query=' # '/sites/1/devices/query?query='
162 |
163 | # Walk the query and filename, construct a URL w/ the query, load up the
164 | # file, call the URL w/ the client, and compare the return data to the
165 | # loaded data.
166 | for query, filename in device_queries:
167 | qs = urllib.quote_plus(query) # 'foo=bar' => 'foo%3Dbar'
168 | url = uri + qs # => /sites/1/devices/query?query=foo%3Dbar
169 | filepath = os.path.join(path, filename) # devices/query/query1.json
170 | output = load_json(filepath)
171 | assert_success(
172 | client.get(url),
173 | output['data']
174 | )
175 |
--------------------------------------------------------------------------------
/tests/model_tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dropbox/hermes/36e5c0532571eed42d5f6edea35f755b837a5b2d/tests/model_tests/__init__.py
--------------------------------------------------------------------------------
/tests/model_tests/fixtures.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from hermes import models
4 |
5 | @pytest.fixture
6 | def db_engine(tmpdir):
7 | db_path = tmpdir.join("hermes.sqlite")
8 | db_engine = models.get_db_engine("sqlite:///%s" % db_path)
9 |
10 | return db_engine
11 |
12 | @pytest.fixture
13 | def session(db_engine, request, tmpdir):
14 | models.Model.metadata.drop_all(db_engine)
15 | models.Model.metadata.create_all(db_engine)
16 | models.Session.configure(bind=db_engine)
17 | session = models.Session()
18 |
19 | models.Fate._all_fates = None
20 |
21 | def fin():
22 | session.close()
23 | request.addfinalizer(fin)
24 |
25 | return session
26 |
27 | @pytest.fixture
28 | def sample_data1(session):
29 | sql_file = open('tests/sample_data/sample_data1.sql')
30 | sql = sql_file.read()
31 | for statement in sql.split(";"):
32 | session.execute(statement)
33 |
34 | return session
35 |
36 | @pytest.fixture
37 | def sample_data2(session):
38 | sql_file = open('tests/sample_data/sample_data2.sql')
39 | sql = sql_file.read()
40 | for statement in sql.split(";"):
41 | session.execute(statement)
42 |
43 | return session
--------------------------------------------------------------------------------
/tests/model_tests/test_events.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from sqlalchemy.exc import IntegrityError
3 |
4 | from hermes import exc
5 | from hermes import models
6 |
7 | from .fixtures import db_engine, session, sample_data1
8 |
9 |
10 | def test_creation(sample_data1):
11 | event_types = sample_data1.query(models.EventType).all()
12 | assert len(event_types) == 7
13 | event_type1 = event_types[0]
14 | assert len(event_type1.events) == 1
15 |
16 | hosts = sample_data1.query(models.Host).all()
17 | assert len(hosts) == 3
18 | host = hosts[0]
19 | assert len(host.events) == 2
20 |
21 | models.Event.create(
22 | sample_data1, host, "testman", event_type1, note="This is a test event"
23 | )
24 | sample_data1.commit()
25 |
26 | events = sample_data1.query(models.Event).all()
27 |
28 | # the total number of events should be 3 now. We care about the new one
29 | assert len(events) == 3
30 | event = events[2]
31 | assert event.id == 3
32 | assert event.host == host
33 | assert event.user == "testman"
34 | assert event.event_type == event_type1
35 | assert event.note == "This is a test event"
36 |
37 | assert len(host.events) == 3
38 | assert len(event_type1.events) == 2
39 |
40 |
41 | def test_duplicate(sample_data1):
42 | """Test to ensure duplicate events are fine b/c there can be multiple identical events"""
43 | event_types = sample_data1.query(models.EventType).all()
44 | assert len(event_types) == 7
45 | event_type1 = event_types[0]
46 |
47 | hosts = sample_data1.query(models.Host).all()
48 | assert len(hosts) == 3
49 | host = hosts[0]
50 |
51 | models.Event.create(
52 | sample_data1, host, "testman", event_type1, note="This is a test event"
53 | )
54 | sample_data1.commit()
55 |
56 | models.Event.create(
57 | sample_data1, host, "testman", event_type1, note="This is another test event"
58 | )
59 | sample_data1.commit()
60 |
61 |
62 | def test_very_large_note(sample_data1):
63 | """Test to ensure notes can be larger than 1024 bytes."""
64 | event_types = sample_data1.query(models.EventType).all()
65 | assert len(event_types) == 7
66 | event_type1 = event_types[0]
67 |
68 | hosts = sample_data1.query(models.Host).all()
69 | assert len(hosts) == 3
70 | host = hosts[0]
71 |
72 | very_large_note = "x" * 60000
73 | models.Event.create(
74 | sample_data1, host, "testman", event_type1, note=very_large_note
75 | )
76 | sample_data1.commit()
77 |
78 | events = sample_data1.query(models.Event).all()
79 |
80 | # the total number of events should be 3 now. We care about the new one
81 | assert len(events) == 3
82 | event = events[2]
83 | assert event.id == 3
84 | assert event.host == host
85 | assert event.user == "testman"
86 | assert event.event_type == event_type1
87 | assert event.note == very_large_note
--------------------------------------------------------------------------------
/tests/model_tests/test_eventtypes.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from sqlalchemy.exc import IntegrityError
3 |
4 | from hermes import exc
5 | from hermes.models import EventType, Host, Event
6 |
7 | from .fixtures import db_engine, session, sample_data1
8 |
9 |
10 | def test_creation(session):
11 | EventType.create(session, "foo", "bar", "This is a test")
12 | session.commit()
13 |
14 | event_types = session.query(EventType).all()
15 |
16 | assert len(event_types) == 1
17 | assert event_types[0].id == 1
18 | assert event_types[0].category == "foo"
19 | assert event_types[0].state == "bar"
20 | assert event_types[0].description == "This is a test"
21 |
22 | event_type = EventType.get_event_type(session, "foo", "bar")
23 | assert event_type.id == 1
24 | assert event_type.category == "foo"
25 | assert event_type.state == "bar"
26 | assert event_type.description == "This is a test"
27 |
28 | assert event_type.href('/test') == '/test/eventtypes/1'
29 |
30 |
31 | def test_duplicate(session):
32 | EventType.create(session, "foo", "bar", "This is a test")
33 |
34 | with pytest.raises(IntegrityError):
35 | EventType.create(session, "foo", "bar", "Desc ignored")
36 |
37 | EventType.create(session, "foo", "bar2", "This is second test")
38 | EventType.create(session, "foo2", "bar", "This is second test")
39 |
40 |
41 | def test_required(session):
42 | EventType.create(session, "foo", "bar", "This is a test")
43 | EventType.create(session, "foo", "bar2")
44 |
45 | with pytest.raises(exc.ValidationError):
46 | EventType.create(session, "foo", None)
47 |
48 | with pytest.raises(exc.ValidationError):
49 | EventType.create(session, None, "bar")
50 |
51 |
52 | def test_get_latest_events(session):
53 | event_type1 = EventType.create(session, "foo", "bar", "test type 1")
54 | event_type2 = EventType.create(session, "foo", "baz", "test type 2")
55 |
56 | host1 = Host.create(session, "server1")
57 | host2 = Host.create(session, "server2")
58 |
59 | Event.create(session, host1, "testman", event_type1)
60 | Event.create(session, host1, "testman", event_type2)
61 | Event.create(session, host2, "testman", event_type1)
62 | Event.create(session, host1, "testman", event_type1)
63 | Event.create(session, host1, "testman", event_type2)
64 | last_type2 = Event.create(session, host2, "testman", event_type2)
65 | last_type1 = Event.create(session, host2, "testman", event_type1)
66 |
67 | events1 = event_type1.get_latest_events().all()
68 | events2 = event_type2.get_latest_events().all()
69 |
70 | assert len(events1) == 4
71 | assert len(events2) == 3
72 |
73 | assert events1[0] == last_type1
74 | assert events2[0] == last_type2
75 |
76 |
--------------------------------------------------------------------------------
/tests/model_tests/test_fates.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from sqlalchemy.exc import IntegrityError
3 |
4 | from hermes import exc
5 | from hermes.models import Fate, EventType
6 |
7 | from .fixtures import db_engine, session, sample_data1
8 |
9 |
10 | def test_creation(sample_data1):
11 | event_types = sample_data1.query(EventType).all()
12 | fates = sample_data1.query(Fate).all()
13 | assert len(event_types) == 7
14 | assert len(fates) == 6
15 |
16 | event_type6 = event_types[5]
17 | event_type7 = event_types[6]
18 |
19 | Fate.create(
20 | sample_data1, event_type6, description="New fate"
21 | )
22 | Fate.create(
23 | sample_data1, event_type7, follows_id=7, description="New fate"
24 | )
25 | sample_data1.commit()
26 |
27 | fates = sample_data1.query(Fate).all()
28 |
29 | # the total number of fates should be 8 now. We care about the new one
30 | assert len(fates) == 8
31 | fate = fates[6]
32 | assert fate.id == 7
33 | assert fate.creation_event_type == event_type6
34 | assert fate.description == "New fate"
35 | assert fate.for_creator is False
36 | assert fate.for_owner is True
37 |
38 | assert len(event_type6.auto_creates) == 1
39 | assert event_type6.auto_creates[0] == fate
40 |
41 | assert len(event_type7.auto_creates) == 1
42 | assert event_type7.auto_creates[0] == fates[7]
43 |
44 |
45 | def test_creation2(sample_data1):
46 | event_types = sample_data1.query(EventType).all()
47 | fates = sample_data1.query(Fate).all()
48 | assert len(event_types) == 7
49 | assert len(fates) == 6
50 |
51 | event_type6 = event_types[5]
52 | event_type7 = event_types[6]
53 |
54 | Fate.create(
55 | sample_data1, event_type6, for_creator=True,
56 | for_owner=False, description="New fate"
57 | )
58 | Fate.create(
59 | sample_data1, event_type7, follows_id=7, description="New fate"
60 | )
61 | sample_data1.commit()
62 |
63 | fates = sample_data1.query(Fate).all()
64 |
65 | # the total number of fates should be 8 now. We care about the new one
66 | assert len(fates) == 8
67 | fate = fates[6]
68 | assert fate.id == 7
69 | assert fate.creation_event_type == event_type6
70 | assert fate.description == "New fate"
71 | assert fate.for_creator is True
72 | assert fate.for_owner is False
73 |
74 | assert len(event_type6.auto_creates) == 1
75 | assert event_type6.auto_creates[0] == fate
76 |
77 | assert len(event_type7.auto_creates) == 1
78 | assert event_type7.auto_creates[0] == fates[7]
79 |
80 |
81 | def test_designation_constraint(sample_data1):
82 | """Fates must be set to for_owner or for_creator or both"""
83 |
84 | event_type1 = sample_data1.query(EventType).get(1)
85 |
86 | with pytest.raises(exc.ValidationError):
87 | Fate.create(
88 | sample_data1, event_type1, description="Wrong fate",
89 | for_creator=False, for_owner=False, follows_id=2
90 | )
91 |
92 |
93 | def test_follows_id_valid(sample_data1):
94 | event_type1 = sample_data1.query(EventType).get(1)
95 |
96 | # There is no Fate 20
97 | with pytest.raises(exc.ValidationError):
98 | Fate.create(
99 | sample_data1, event_type1, description="Coolio",
100 | follows_id=20
101 | )
--------------------------------------------------------------------------------
/tests/model_tests/test_host.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from sqlalchemy.exc import IntegrityError
3 |
4 | from hermes import exc
5 | from hermes.models import Host, EventType, Labor, Event
6 |
7 | from .fixtures import db_engine, session, sample_data1
8 |
9 |
10 | def test_creation(session):
11 | Host.create(session, "abc-123")
12 | session.commit()
13 |
14 | hosts = session.query(Host).all()
15 |
16 | assert len(hosts) == 1
17 | assert hosts[0].id == 1
18 | assert hosts[0].hostname == "abc-123"
19 |
20 | host = Host.get_host(session, "abc-123")
21 | assert host.id == 1
22 | assert host.hostname == "abc-123"
23 |
24 |
25 | def test_duplicate(session):
26 | Host.create(session, "abc-123")
27 |
28 | with pytest.raises(IntegrityError):
29 | Host.create(session, "abc-123")
30 |
31 | Host.create(session, "abc-456")
32 |
33 |
34 | def test_required(session):
35 | Host.create(session, "abc-123")
36 |
37 | with pytest.raises(exc.ValidationError):
38 | Host.create(session, None)
39 |
40 |
41 | def test_get_latest_events(sample_data1):
42 | host = Host.get_host(sample_data1, "example.dropbox.com")
43 | assert host.id == 1
44 | assert host.hostname == "example.dropbox.com"
45 |
46 | events = host.get_latest_events().all()
47 |
48 | assert len(events) == 2
49 | assert events[0].note == "example.dropbox.com rebooted."
50 |
51 |
52 | def test_get_labors(sample_data1):
53 | host = Host.get_host(sample_data1, "example.dropbox.com")
54 | assert host.id == 1
55 | assert len(host.labors) == 0
56 |
57 | event_type1 = sample_data1.query(EventType).get(1)
58 | event_type3 = sample_data1.query(EventType).get(3)
59 | event_type4 = sample_data1.query(EventType).get(4)
60 |
61 | print "Creating event1"
62 | Event.create(sample_data1, host, "testman", event_type1)
63 |
64 | print "Creating event2"
65 | Event.create(sample_data1, host, "testman", event_type3)
66 |
67 | print "Creating event3"
68 | closing_event = Event.create(sample_data1, host, "testman", event_type4)
69 |
70 | print "Get labor info"
71 | all_labors = host.get_labors().all()
72 | open_labors = host.get_open_labors().all()
73 |
74 | assert len(all_labors) == 3
75 | assert len(host.labors) == 3
76 | assert len(open_labors) == 1
77 |
78 | assert all_labors[0].completion_time is None
79 | assert all_labors[0].completion_event is None
80 | assert all_labors[1].completion_time is not None
81 | assert all_labors[1].completion_event == closing_event
82 | assert all_labors[0].creation_event == closing_event
83 |
84 |
85 |
--------------------------------------------------------------------------------
/tests/model_tests/test_labors.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from sqlalchemy import desc
4 | from sqlalchemy.exc import IntegrityError
5 |
6 | from hermes import exc
7 | from hermes.models import Event, EventType, Fate, Host, Labor
8 |
9 | from .fixtures import db_engine, session, sample_data1, sample_data2
10 |
11 |
12 | def test_lifecycle1(sample_data1):
13 | """Test the automatic creation and closing of labors based on Events and Fates
14 |
15 | Throw event A, and see that it creates Labor A.
16 | Throw event B, and see that it closes Labor A and creates no new Labors.
17 | """
18 | labors = sample_data1.query(Labor).all()
19 | assert len(labors) == 0
20 |
21 | event_type_a = sample_data1.query(EventType).get(1)
22 | event_type_b = sample_data1.query(EventType).get(2)
23 | host = sample_data1.query(Host).first()
24 | assert len(host.labors) == 0
25 |
26 | # Throw event A
27 | Event.create(sample_data1, host, "system", event_type_a)
28 |
29 | event = (
30 | sample_data1.query(Event)
31 | .order_by(desc(Event.id)).first()
32 | )
33 |
34 | assert event.host == host
35 | assert event.event_type == event_type_a
36 |
37 | labors = sample_data1.query(Labor).all()
38 | assert len(labors) == 1
39 | assert labors[0].completion_time is None
40 | assert labors[0].completion_event is None
41 | assert labors[0].creation_event == event
42 | assert labors[0].for_creator is False
43 | assert len(host.labors) == 1
44 | assert len(event.created_labors) == 1
45 | assert len(event.completed_labors) == 0
46 |
47 | # Throw event B
48 | Event.create(
49 | sample_data1, host, "system", event_type_b
50 | )
51 |
52 | event = (
53 | sample_data1.query(Event)
54 | .order_by(desc(Event.id)).first()
55 | )
56 |
57 | assert event.host == host
58 | assert event.event_type == event_type_b
59 |
60 | labors = sample_data1.query(Labor).all()
61 | assert len(labors) == 1
62 | assert labors[0].completion_time is not None
63 | assert labors[0].completion_event == event
64 | assert labors[0].for_creator is False
65 | assert len(event.created_labors) == 0
66 | assert len(event.completed_labors) == 1
67 |
68 | assert len(host.labors) == 1
69 |
70 |
71 | def test_lifecycle_simple2(sample_data1):
72 | """Test another simple lifecycle
73 |
74 | Throw event A, and see that it creates Labor A.
75 | Throw event D, and see that it closes Labor A and creates no new labors.
76 | """
77 | labors = sample_data1.query(Labor).all()
78 | assert len(labors) == 0
79 |
80 | host = sample_data1.query(Host).first()
81 | assert len(host.labors) == 0
82 |
83 | event_a = sample_data1.query(EventType).get(1)
84 | event_d = sample_data1.query(EventType).get(4)
85 |
86 | # Throw event A
87 | Event.create(sample_data1, host, "system", event_a)
88 |
89 | event = (
90 | sample_data1.query(Event)
91 | .order_by(desc(Event.id)).first()
92 | )
93 |
94 | assert event.host == host
95 | assert event.event_type == event_a
96 |
97 | labors = sample_data1.query(Labor).all()
98 | assert len(labors) == 1
99 | assert labors[0].completion_time is None
100 | assert labors[0].completion_event is None
101 | assert labors[0].creation_event == event
102 | assert labors[0].for_creator is False
103 | assert len(host.labors) == 1
104 | assert len(event.created_labors) == 1
105 | assert len(event.completed_labors) == 0
106 |
107 |
108 | # Throw event D
109 | Event.create(
110 | sample_data1, host, "system", event_d
111 | )
112 |
113 | event = (
114 | sample_data1.query(Event)
115 | .order_by(desc(Event.id)).first()
116 | )
117 |
118 | assert event.host == host
119 | assert event.event_type == event_d
120 |
121 | labors = sample_data1.query(Labor).all()
122 | assert len(labors) == 1
123 | assert labors[0].completion_time is not None
124 | assert labors[0].completion_event == event
125 | assert labors[0].for_creator is False
126 | assert len(event.created_labors) == 0
127 | assert len(event.completed_labors) == 1
128 |
129 | assert len(host.labors) == 1
130 |
131 |
132 | def test_lifecycle_complex2(sample_data1):
133 | """Test the automatic creation and closing of labors based on Events and Fates.
134 | This version is a bit more complex in that we make sure unaffiliated labors
135 | are left untouched.
136 |
137 | Throw event A, creates Labor A.
138 | Throw event C, creates Labor C.
139 | Throw event B, closes Labor A, but does nothing to Labor C.
140 | """
141 | labors = sample_data1.query(Labor).all()
142 | assert len(labors) == 0
143 |
144 | fates = sample_data1.query(Fate).all()
145 | fate1 = fates[0]
146 | fate2 = fates[1]
147 | fate4 = fates[3]
148 |
149 | hosts = sample_data1.query(Host).all()
150 | host1 = hosts[0]
151 | host2 = hosts[1]
152 |
153 | # Throw event A and C
154 | Event.create(sample_data1, host1, "system", fate1.creation_event_type)
155 | Event.create(sample_data1, host2, "system", fate4.creation_event_type)
156 |
157 | event = (
158 | sample_data1.query(Event)
159 | .order_by(desc(Event.id)).first()
160 | )
161 |
162 | assert event.host == host2
163 | assert event.event_type == fate4.creation_event_type
164 |
165 | labors = sample_data1.query(Labor).all()
166 | assert len(labors) == 2
167 | assert labors[0].completion_time is None
168 | assert labors[0].completion_event is None
169 | assert labors[0].for_creator is False
170 | assert labors[1].completion_time is None
171 | assert labors[1].completion_event is None
172 | assert labors[1].for_creator is False
173 |
174 | # Throw event B
175 | Event.create(
176 | sample_data1, host1, "system", fate2.creation_event_type
177 | )
178 |
179 | event = (
180 | sample_data1.query(Event)
181 | .order_by(desc(Event.id)).first()
182 | )
183 |
184 | assert event.host == host1
185 | assert event.event_type == fate2.creation_event_type
186 |
187 | labors = sample_data1.query(Labor).all()
188 | assert len(labors) == 2
189 | assert labors[0].completion_time is not None
190 | assert labors[0].completion_event is not None
191 | assert labors[0].for_creator is False
192 | assert labors[1].completion_time is None
193 | assert labors[1].completion_event is None
194 | assert labors[1].for_creator is False
195 |
196 | labors = Labor.get_open_labors(sample_data1).all()
197 | assert len(labors) == 1
198 |
199 | labors = Labor.get_open_unacknowledged(sample_data1)
200 | assert len(labors) == 1
201 |
202 |
203 | def test_acknowledge(sample_data1):
204 | """Test to ensure that acknowledgement correctly flags Labors as such"""
205 |
206 | labors = sample_data1.query(Labor).all()
207 | assert len(labors) == 0
208 |
209 | fate = sample_data1.query(Fate).get(1)
210 | host = sample_data1.query(Host).get(1)
211 |
212 | Event.create(sample_data1, host, "system", fate.creation_event_type)
213 |
214 | event = (
215 | sample_data1.query(Event)
216 | .order_by(desc(Event.id)).first()
217 | )
218 |
219 | assert event.host == host
220 | assert event.event_type == fate.creation_event_type
221 |
222 | labors = Labor.get_open_unacknowledged(sample_data1)
223 | assert len(labors) == 1
224 | assert labors[0].completion_time is None
225 | assert labors[0].completion_event is None
226 | assert labors[0].ack_time is None
227 | assert labors[0].ack_user is None
228 | assert labors[0].creation_event == event
229 | assert labors[0].for_creator is False
230 |
231 | labors[0].acknowledge("testman")
232 |
233 | labors = sample_data1.query(Labor).all()
234 | assert len(labors) == 1
235 | assert labors[0].completion_time is None
236 | assert labors[0].completion_event is None
237 | assert labors[0].ack_time is not None
238 | assert labors[0].ack_user == "testman"
239 | assert labors[0].creation_event == event
240 | assert labors[0].for_creator is False
241 |
242 | labors = Labor.get_open_unacknowledged(sample_data1)
243 | assert len(labors) == 0
244 |
245 |
246 | def test_cannot_start_in_midworkflow(sample_data1):
247 | """Ensures that intermediate fates do not create labors when no labor
248 | exists.
249 |
250 | Given a Fate C -> D, and intermediate Fate D -> E,
251 | Throw event D and ensure Labor D is not created since Labor C does not exist.
252 |
253 | """
254 |
255 | labors = sample_data1.query(Labor).all()
256 | assert len(labors) == 0
257 |
258 | event_type_d = sample_data1.query(EventType).get(4)
259 | host = sample_data1.query(Host).get(1)
260 |
261 | Event.create(sample_data1, host, "system", event_type_d)
262 |
263 | event = (
264 | sample_data1.query(Event)
265 | .order_by(desc(Event.id)).first()
266 | )
267 |
268 | assert event.host == host
269 | assert event.event_type == event_type_d
270 |
271 | labors = Labor.get_open_unacknowledged(sample_data1)
272 | assert len(labors) == 0
273 |
274 |
275 | def test_longer_chain(sample_data2):
276 | """Test chained labors A->B->C->D"""
277 | labors = sample_data2.query(Labor).all()
278 | assert len(labors) == 0
279 |
280 | # system-maintenance audit
281 | event_type_a = sample_data2.query(EventType).get(1)
282 | # system-maintenance needed
283 | event_type_b = sample_data2.query(EventType).get(2)
284 | # system-maintenance ready
285 | event_type_c = sample_data2.query(EventType).get(3)
286 | # system-maintenance completed
287 | event_type_d = sample_data2.query(EventType).get(4)
288 |
289 | host = sample_data2.query(Host).get(1)
290 |
291 | event_a = Event.create(sample_data2, host, "system", event_type_a)
292 |
293 | # We will aggressively validate the events created only for event A
294 | event = (
295 | sample_data2.query(Event)
296 | .order_by(desc(Event.id)).first()
297 | )
298 | assert event == event_a
299 | assert event.host == host
300 | assert event.event_type == event_type_a
301 |
302 | labors = Labor.get_open_unacknowledged(sample_data2)
303 | assert len(labors) == 1
304 | assert len(host.labors) == 1
305 | assert labors[0].starting_labor_id is None
306 | assert labors[0].for_creator is False
307 | starting_labor_id = labors[0].id
308 |
309 | event_b = Event.create(sample_data2, host, "system", event_type_b)
310 | labors = Labor.get_open_unacknowledged(sample_data2)
311 | assert len(labors) == 1
312 | assert len(host.labors) == 2
313 | assert labors[0].starting_labor_id == starting_labor_id
314 | assert labors[0].for_creator is False
315 |
316 | event_c = Event.create(sample_data2, host, "system", event_type_c)
317 | labors = Labor.get_open_unacknowledged(sample_data2)
318 | assert len(labors) == 1
319 | assert len(host.labors) == 3
320 | assert labors[0].starting_labor_id == starting_labor_id
321 | assert labors[0].for_creator is True
322 |
323 | # This last event closes the final labor but does not create a new labor
324 | event_d = Event.create(sample_data2, host, "system", event_type_d)
325 | labors = Labor.get_open_unacknowledged(sample_data2)
326 | assert len(labors) == 0
327 | assert len(host.labors) == 3
328 |
329 |
330 |
331 |
332 |
333 |
--------------------------------------------------------------------------------
/tests/sample_data/sample_data1.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO event_types
2 | VALUES
3 | (1,'system-reboot','required','This system requires a reboot.', 0);
4 |
5 | INSERT INTO event_types
6 | VALUES
7 | (2,'system-reboot','completed','This system rebooted.', 0);
8 |
9 | INSERT INTO event_types
10 | VALUES
11 | (3,'system-maintenance','required','This system requires maintenance.', 0);
12 |
13 | INSERT INTO event_types
14 | VALUES
15 | (4,'system-maintenance','ready','This system is ready for maintenance.', 0);
16 |
17 | INSERT INTO event_types
18 | VALUES
19 | (5,'system-maintenance','completed','System maintenance completed.', 0);
20 |
21 | INSERT INTO event_types
22 | VALUES
23 | (6,'system-shutdown','required','System shutdown required.', 0);
24 |
25 | INSERT INTO event_types
26 | VALUES
27 | (7,'system-shutdown','completed','System shutdown completed.', 0);
28 |
29 | INSERT INTO fates
30 | VALUES
31 | (1,1,NULL, 0, 1, 'Reboot or release the system');
32 |
33 | INSERT INTO fates
34 | VALUES
35 | (2,2,1, 0, 1, 'System rebooted');
36 |
37 | INSERT INTO fates
38 | VALUES
39 | (3,4,1, 0, 1, 'System released');
40 |
41 | INSERT INTO fates
42 | VALUES
43 | (4,3,NULL, 0, 1, 'Release or acknowledge downtime');
44 |
45 | INSERT INTO fates
46 | VALUES
47 | (5,4,4, 1, 0, 'Perform maintenance');
48 |
49 | INSERT INTO fates
50 | VALUES
51 | (6,5,5, 1, 0, 'Maintenance completed');
52 |
53 |
54 | INSERT INTO hosts
55 | VALUES (1, 'example.dropbox.com');
56 |
57 | INSERT INTO hosts
58 | VALUES (2, 'sample.dropbox.com');
59 |
60 | INSERT INTO hosts
61 | VALUES (3, 'test.dropbox.com');
62 |
63 | INSERT INTO events ('id', 'host_id', 'timestamp', 'user', 'event_type_id', 'note')
64 | VALUES (1, 1, "2015-04-31 10:00:00", "system", 1, "example.dropbox.com needs a reboot");
65 |
66 | INSERT INTO events ('id', 'host_id', 'timestamp', 'user', 'event_type_id', 'note')
67 | VALUES (2, 1, "2015-05-01 22:34:03", "system", 2, "example.dropbox.com rebooted.");
68 |
--------------------------------------------------------------------------------
/tests/sample_data/sample_data2.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO event_types
2 | VALUES
3 | (1,'system-maintenance','audit','Audit required', 0);
4 |
5 | INSERT INTO event_types
6 | VALUES
7 | (2,'system-maintenance','needed','This system needs maintenance.', 0);
8 |
9 | INSERT INTO event_types
10 | VALUES
11 | (3,'system-maintenance','ready','This is ready for maintenance.', 0);
12 |
13 | INSERT INTO event_types
14 | VALUES
15 | (4,'system-maintenance','completed','System maintenance completed.', 0);
16 |
17 | INSERT INTO event_types
18 | VALUES
19 | (5,'system-reboot','needed','System reboot required.', 0);
20 |
21 | INSERT INTO event_types
22 | VALUES
23 | (6,'system-reboot','completed','System has rebooted.', 0);
24 |
25 | INSERT INTO event_types
26 | VALUES
27 | (7,'puppet','restart','Puppet has restarted.', 0);
28 |
29 | INSERT INTO fates
30 | VALUES
31 | (1,1,null, 0, 1, 'Audit the system.');
32 |
33 | INSERT INTO fates
34 | VALUES
35 | (2,2,1, 0, 1, 'Reboot, release the system, or authorize downtime.');
36 |
37 | INSERT INTO fates
38 | VALUES
39 | (3,3,2, 1, 0, 'Perform maintenance');
40 |
41 | INSERT INTO fates
42 | VALUES
43 | (4,4,3, 1, 0, 'Maintenance completed');
44 |
45 | INSERT INTO fates
46 | VALUES
47 | (5,6,2, 0, 1, 'System rebooted to finish maintenance');
48 |
49 | INSERT INTO fates
50 | VALUES
51 | (6,5, NULL, 0, 1, 'Reboot the system.');
52 |
53 | INSERT INTO fates
54 | VALUES
55 | (7,6, 6, 0, 1, 'Restart puppet');
56 |
57 | INSERT INTO fates
58 | VALUES
59 | (8,7,7, 1, 0, 'Puppet restarted');
60 |
61 | INSERT INTO fates
62 | VALUES
63 | (9,1,null, 0, 1, 'Release or restart puppet');
64 |
65 | INSERT INTO fates
66 | VALUES
67 | (10,7,9, 0, 1, 'System is fine. Puppet restarted.');
68 |
69 | INSERT INTO hosts
70 | VALUES (1, 'example.dropbox.com');
71 |
72 | INSERT INTO hosts
73 | VALUES (2, 'sample.dropbox.com');
74 |
75 | INSERT INTO hosts
76 | VALUES (3, 'test.dropbox.com');
--------------------------------------------------------------------------------