Skip to content

Commit

Permalink
Merge pull request #104 from diggyk/explicit_chain
Browse files Browse the repository at this point in the history
Explicit fate chain
  • Loading branch information
jathanism committed Dec 8, 2015
2 parents ac39f31 + b51328a commit 4268d8a
Show file tree
Hide file tree
Showing 13 changed files with 263 additions and 137 deletions.
107 changes: 69 additions & 38 deletions bin/hermes
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,17 @@ from hermes.settings_client import settings
logging.getLogger("requests").setLevel(logging.WARNING)
urllib3.disable_warnings()


class HermesException(Exception):
"""Generic exception used to indicate a problem with a Hermes operation"""
pass


class HermesNotFound(Exception):
"""Thrown when something is not found"""
pass


def retry(num_attempts, min_sleep=.1, max_sleep=5):
for num in range(1, num_attempts+1):
yield num
Expand All @@ -44,6 +51,7 @@ def retry(num_attempts, min_sleep=.1, max_sleep=5):

sleep(sleep_time)


def request_get(path):
"""Make an HTTP GET request for the given path
Expand All @@ -57,6 +65,11 @@ def request_get(path):
if response.status_code < 500:
break

if response.status_code == requests.codes.not_found:
raise HermesNotFound("{} returned 404".format(
settings.hermes_server + path)
)

if response.status_code != requests.codes.ok or not response.content:
try:
data = response.json()["error"]["message"]
Expand Down Expand Up @@ -86,6 +99,11 @@ def request_post(path, json):
if response.status_code < 500:
break

if response.status_code == requests.codes.not_found:
raise HermesNotFound("{} returned 404".format(
settings.hermes_server + path)
)

if (
response.status_code != requests.codes.created
or not response.content
Expand Down Expand Up @@ -117,6 +135,11 @@ def request_put(path, json):
if response.status_code < 500:
break

if response.status_code == requests.codes.not_found:
raise HermesNotFound("{} returned 404".format(
settings.hermes_server + path)
)

if (
response.status_code != requests.codes.ok
or not response.content
Expand Down Expand Up @@ -261,13 +284,14 @@ def list_fates(args):
print "FATES: \n(id) created by => completed by\n"
for fate in fates:
if not fate.get("precedesIds"):
print "({}) {}".format(
print " ({}) {}".format(
fate["id"],
fate["creationEventType"]["category"] + " "
+ fate["creationEventType"]["state"]
)
else:
print "({}) {} => {}".format(
print "{} ({}) {} => {}".format(
"*" if not fate["followsId"] else " ",
fate["id"],
fate["creationEventType"]["category"] + " "
+ fate["creationEventType"]["state"],
Expand All @@ -280,6 +304,26 @@ def list_fates(args):
)
print ""

print "FATES that can start labors/quests:"
for fate in fates:
if not fate["followsId"]:
print "* ({}) {} => {}".format(
fate["id"],
fate["creationEventType"]["category"] + " "
+ fate["creationEventType"]["state"],
fate["precedesIds"]
)

print textwrap.fill(
fate["description"], width=60,
initial_indent="\t", subsequent_indent="\t"
)

print ""

print "For a graphical view, visit:"
print "\t{}/v1/fates".format(settings.hermes_server)


def create_fate(args):
logging.debug("create_fates()")
Expand Down Expand Up @@ -502,7 +546,7 @@ def list_host_labors_monitoring(args):
for labor in labors:
if labor['creationEvent']['eventType']['state'] == "required":
alert = True
# determin if critical
# determine if critical
if labor['quest']['targetTime']:
target_time = parser.parse(labor['quest']['targetTime'], yearfirst=True)
target_time = target_time.replace(tzinfo=tz.tzlocal())
Expand All @@ -512,23 +556,18 @@ def list_host_labors_monitoring(args):
if time_left.days <= 5:
critical = True

except HermesException as exc:
except HermesNotFound as exc:
# FIXME -- add error codes to Hermes API and test for that
if exc.message.startswith("Error: No host"):
# if the error was that the hostname wasn't found, just return
# an empty list because hermes doesn't know about this host and
# so we can assume it has no open labors
labors = []
else:
print "UNKNOWN: Querying Hermes returned an exception"
print "UNKNOWN: 404 querying Hermes returned an exception"
print ""
traceback.print_exc(file=sys.stdout)
sys.exit(3)
except Exception as exc:
print "UNKNOWN: Querying Hermes returned an exception"
print ""
traceback.print_exc(file=sys.stdout)
sys.exit(3)

response = request_get("/api/v1/fates?limit=all&expand=eventtypes")
fates = response.json()["fates"]
Expand Down Expand Up @@ -794,8 +833,8 @@ def show_quest(args):
def create_quest(args):
logging.debug("create_quest()")
logging.debug(
"category: %s state: %s note: %s",
args.category, args.state, args.description
"fate: %s note: %s",
args.fate_id, args.description
)

print "Creating quest... (could take some time)"
Expand All @@ -817,28 +856,26 @@ def create_quest(args):
logging.error("No hosts specified")
return

response = request_get("/api/v1/eventtypes?limit=all")
event_types = response.json()["eventTypes"]

found_event_type = None
for event_type in event_types:
if (
event_type["category"] == args.category
and event_type["state"] == args.state
):
found_event_type = event_type
try:
response = request_get("/api/v1/fates/{}".format(args.fate_id))
fate_json = response.json()

if found_event_type is None:
except HermesNotFound:
sys.exit(
"No matching event type found for {} {}".format(
args.category, args.state
"ERROR: No matching fate found for id {}".format(
args.fate_id
)
)

if fate_json["followsId"] is not None:
sys.exit(
"ERROR: Fate {} is not a valid starting fate".format(args.fate_id)
)

user = getpass.getuser()

json = {
"eventTypeId": found_event_type["id"],
"fateId": args.fate_id,
"creator": user,
"description": args.description
}
Expand Down Expand Up @@ -1108,25 +1145,19 @@ def parse_cli_args():
"create",
description="To create a Hermes Quest, one should first decide two "
"things: (1) what hosts will be part of this Quest, "
"and (2) what the Event Type will be for the Events "
"that start the Quest.\n"
"and (2) what Fate will start the Labors for the hosts"
"in the the Quest.\n"
"\n"
"Hosts can be specified by a query string sent to an "
"external source of record. Check with your Hermes "
"administrator for more information.\n"
"\n"
"To pick the proper Event Type, use `hermes fate list` "
"and pick from the \"created by\" column."
)
quest_create_parser.add_argument(
"category",
help="The Event category for the Event "
"that will be created for each Host."
"To pick the proper Fate ID, use `hermes fate list` "
"and pick an ID designated with a *."
)
quest_create_parser.add_argument(
"state",
help="The Event state for the Event "
"that will be created for each Host."
"--fate-id", type=int, dest="fate_id",
help="The ID of the Fate that will create the starting Labors"
)
quest_create_parser.add_argument(
"--due", type=str, metavar='YYYY-MM-DD HH:MM',
Expand Down
1 change: 1 addition & 0 deletions db/update_to_0_5_15.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
drop index _creation_completion_uc on fates;
14 changes: 7 additions & 7 deletions hermes/handlers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1879,7 +1879,7 @@ def post(self):
Host: localhost
Content-Type: application/json
{
"eventTypeId": 1,
"fateId": 1,
"creator": "johnny",
"targetTime": timestamp,
"description": "This is a quest almighty",
Expand Down Expand Up @@ -1927,7 +1927,7 @@ def post(self):
log.info("Creating a new quest")

try:
event_type_id = self.jbody["eventTypeId"]
fate_id = self.jbody["fateId"]
creator = self.jbody["creator"]
if not EMAIL_REGEX.match(creator):
creator += "@" + self.domain
Expand All @@ -1952,12 +1952,12 @@ def post(self):
except ValueError as err:
raise exc.BadRequest(err.message)

event_type = (
self.session.query(EventType).get(event_type_id)
fate = (
self.session.query(Fate).get(fate_id)
)

if event_type is None:
self.write_error(400, message="Bad creation event type")
if fate is None:
self.write_error(400, message="Bad fate id {}".format(fate_id))
return

# If a host query was specified, we need to talk to the external
Expand Down Expand Up @@ -1992,7 +1992,7 @@ def post(self):

try:
quest = Quest.create(
self.session, creator, hosts, event_type, target_time,
self.session, creator, hosts, target_time, fate_id=fate_id,
description=description
)
except IntegrityError as err:
Expand Down
37 changes: 21 additions & 16 deletions hermes/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,11 @@ class Fate(Model):
_starting_fates = cached list of all non-intermediate Fates
Notes:
A Fate can create a Labor can be designated for both the server owner and the quest owner
A Fate can create a Labor can be designated for both the server owner and the quest owner.
Because we can have similar but differing chains, we can have duplicate Fates
because they might be used to tie together different flows. For instance,
we may want an ability to force an A -> B -> D chain instead of an A -> B -> C -> D
chain, in which case, there would be two As, Bs, and Ds.
"""

__tablename__ = "fates"
Expand Down Expand Up @@ -556,10 +560,6 @@ class Fate(Model):

description = Column(String(2048), nullable=True)
__table_args__ = (
UniqueConstraint(
creation_type_id, follows_id,
name='_creation_completion_uc'
),
Index(
"fate_idx", id, creation_type_id, follows_id
),
Expand Down Expand Up @@ -728,7 +728,9 @@ def question_the_fates(cls, session, events, quest=None, starting_fates=None):
# Get all the fates, in various categories, for easy reference
all_fates = Fate.get_all_fates(session)
if not starting_fates:
starting_fates = Fate.get_starting_fates(session)
starting_fates = session.query(Fate).filter(
Fate.follows_id == None
)

# Query the database for open labors for hosts of which we have an event
open_labors = (
Expand Down Expand Up @@ -756,17 +758,19 @@ def question_the_fates(cls, session, events, quest=None, starting_fates=None):
# First, lets see if this Event is supposed to create any
# non-intermediate Labors and add them to the batch
for fate in starting_fates:
if fate["creation_type_id"] == event_type.id:
if fate.creation_type_id == event_type.id:
new_labor_dict = {
"host_id": host.id,
"creation_event_id": event.id,
"fate_id": fate["id"],
"fate_id": fate.id,
"quest_id": quest.id if quest else None,
"for_creator": fate["for_creator"],
"for_owner": fate["for_owner"]
"for_creator": fate.for_creator,
"for_owner": fate.for_owner
}
if new_labor_dict not in all_new_labors:
all_new_labors.append(new_labor_dict)
# we only want to match up to the first fate found
break

# Now let's see if we should be closing any labors.
# We will see what fate created a labor, then examine all the fates
Expand Down Expand Up @@ -1062,8 +1066,8 @@ class Quest(Model):

@classmethod
def create(
cls, session, creator, hosts, creation_event_type,
target_time=None, create=True, description=None, fate_id=None
cls, session, creator, hosts, target_time=None, create=True,
description=None, fate_id=None
):
"""Create a new Quest.
Expand All @@ -1081,7 +1085,7 @@ def create(
session: an active database session
creator: the person or system creating the Quest
hosts: a list of Hosts for which to create Events (and Labors)
creation_event_type: the EventType of which to create Events
fate_id: the explicit Fate for which to create events and labors
target_time: the optional targeted date and time of Quest completion
create: if True, Events will be created; if False, reclaim existing Labors
description: a required human readable text to describe this Quest
Expand All @@ -1092,11 +1096,12 @@ def create(
raise exc.ValidationError("Quest target date must be in future")
if hosts is None:
raise exc.ValidationError("Quest must have a list of hosts")
if creation_event_type is None:
raise exc.ValidationError("Quest must have an EventType")
if fate_id is None:
raise exc.ValidationError("Quest must have a Fate")

if fate_id:
fate = session.query(Fate).id(fate_id)
fate = session.query(Fate).get(fate_id)
creation_event_type = fate.creation_event_type
else:
fate = None

Expand Down
2 changes: 1 addition & 1 deletion hermes/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.6.9"
__version__ = "0.7.0"
2 changes: 1 addition & 1 deletion hermes/webapp/src/js/controllers/questCreationCtrl.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
}

vm.result = hermesService.createQuest(vm.user, vm.hostList,
vm.selectedFate.creationEventType, vm.targetDate, vm.description)
vm.selectedFate.id, vm.targetDate, vm.description)
.then(function(response) {
vm.createInProgress = false;
vm.hostList = [];
Expand Down
6 changes: 3 additions & 3 deletions hermes/webapp/src/js/services/hermesService.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@
* Try to create a quest given the information
* @param user the user who is the owner/creator of the quest
* @param hosts the list of hosts to which this quest applies
* @param eventType the starting event-type
* @param fateId the ID for the Fate to use to start the quest
* @param targetDateTime the date and time the quest should complete
* @param description the human readable description for this quest
*/
function createQuest(user, hosts, eventType, targetDateTime, description) {
function createQuest(user, hosts, fateId, targetDateTime, description) {
return $http.post("/api/v1/quests", {
'creator': user,
'hostnames': hosts,
'eventTypeId': eventType.id,
'fateId': fateId,
'targetTime': targetDateTime,
'description': description
}).then(createQuestCompleted)
Expand Down

0 comments on commit 4268d8a

Please sign in to comment.