You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

392 lines
17 KiB
Python

from flask import Flask, request
import os
import datetime
7 years ago
import subprocess
import re
import random
import mattermostdriver
import event
import slugify
7 years ago
random.seed( os.urandom( 8 ) )
class Mattermost( object ):
def __init__( self, *args, **kwargs ):
self.mm = mattermostdriver.Driver( kwargs )
self.mm.login()
7 years ago
def strip_ansi( text, escape = re.compile( r'\x1B\[[0-?]*[ -/]*[@-~]' ) ):
return escape.sub( '', text )
def strip_progress( text, escape = re.compile( r'\r[^\n]*\n' ) ):
return escape.sub( '', text )
app = Flask( __name__ )
app.config.from_envvar( 'FIH_SETTINGS' )
app.mm = Mattermost( url = app.config[ 'MATTERMOST_URL' ],
port = app.config[ 'MATTERMOST_PORT' ],
login_id = app.config[ 'MATTERMOST_USER_EMAIL' ],
password = app.config[ 'MATTERMOST_USER_PASSWORD' ],
).mm
def generate_raw_incident_number():
return datetime.datetime.now().strftime( "%Y%m%d" ) + ( "%4d" % int( random.random() * 10000 ) )
def raw_incident_number_to_channel_name( incident_number ):
return app.config[ "MATTERMOST_INCIDENT_CHANNEL_PREFIX" ] + incident_number
def raw_incident_number_to_public_incident_number( incident_number ):
return app.config[ "PUBLIC_INCIDENT_PREFIX" ] + incident_number
def raw_incident_number_to_url( incident_number ):
return app.config[ "INCIDENT_URL_SCHEME" ] % incident_number
def is_incident_channel_name( channel_name ):
return channel_name.startswith( app.config[ "MATTERMOST_INCIDENT_CHANNEL_PREFIX" ] )
def channel_name_to_raw_incident_number( channel_name ):
return channel_name[ len( app.config[ "MATTERMOST_INCIDENT_CHANNEL_PREFIX" ] ): ]
def mattermost_incident_command( command, args, channel_id, raw_incident_number, user_name ):
channel = app.mm.channels.get_channel( channel_id )
slug = slugify.slugify( channel[ 'purpose' ] )
index = event.Index( path = app.config[ "STATUS_EVENT_INDEX" ] )
ev = event.Event( path = app.config[ "STATUS_EVENT_SCHEME" ] % { "incident_number": raw_incident_number,
"slug": slug,
},
glob = app.config[ "STATUS_EVENT_GLOB" ] % { "incident_number": raw_incident_number,
},
)
if command == "PUBLISH":
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': "### `PUBLISH` requested by @%s!" % user_name,
} )
# update local repository
cmdline = app.config[ "MERCURIAL_BIN" ] + " update"
message = exec_to_message( cmdline )
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': message,
} )
# create/update event content file
new = ev.open()
ev.doc = channel[ 'header' ]
ev.set_title( channel[ 'purpose' ] )
ev.set_slug( raw_incident_number )
ev.set_published( datetime.datetime.utcnow().strftime( "%Y-%m-%d %H:%M" ) )
written = ev.write()
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': "#### Written `%s`:\n\n```\n%s```" % ( ev.path_short(), written ),
} )
cmdline = app.config[ "MERCURIAL_BIN" ] + " add " + ev.path_short()
message = exec_to_message( cmdline )
if message:
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': message,
} )
cmdline = app.config[ "MERCURIAL_BIN" ] + " commit -m 'event for incident " + raw_incident_number + " via FIH' " + ev.path_short()
message = exec_to_message( cmdline )
if message:
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': message,
} )
# pull, merge, commit
for pull_from in app.config[ "MERCURIAL_PUSH_TO" ]:
cmdline = app.config[ "MERCURIAL_BIN" ] + " pull " + pull_from
message = exec_to_message( cmdline )
if message:
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': message,
} )
cmdline = app.config[ "MERCURIAL_BIN" ] + " merge"
message = exec_to_message( cmdline )
if message:
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': message,
} )
cmdline = app.config[ "MERCURIAL_BIN" ] + " commit -m 'merge from " + pull_from + "'"
message = exec_to_message( cmdline )
if message:
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': message,
} )
# push updated repository
for pull_from in app.config[ "MERCURIAL_PUSH_TO" ]:
cmdline = app.config[ "MERCURIAL_BIN" ] + " push " + pull_from
message = exec_to_message( cmdline )
if message:
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': message,
} )
for deploy_to in app.config[ "GROW_DEPLOY_TO" ]:
cmdline = app.config[ "GROW_BIN" ] + " deploy -f " + deploy_to
message = exec_to_message( cmdline )
if message:
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': message,
} )
app.mm.posts.create_post( options = { 'channel_id': request.form[ 'channel_id' ],
'message': "### `PUBLISH` completed!",
} )
elif command == "START":
ev.open()
started = ev.set_started( args or datetime.datetime.utcnow().strftime( "%Y-%m-%d %H:%M" ) )
ev.write()
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': "Event start date set by %s to: `%s`" % ( user_name, started ),
} )
elif command == "ETR":
ev.open()
expected = ev.set_expected( args or datetime.datetime.utcnow().strftime( "%Y-%m-%d %H:%M" ) )
ev.write()
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': "Event estimated time of resolution set by %s to: `%s`" % ( user_name, expected ),
} )
elif command == "FINISHED":
ev.open()
finished = ev.set_finished( args or datetime.datetime.utcnow().strftime( "%Y-%m-%d %H:%M" ) )
ev.write()
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': "Event finished time set by %s to: `%s`" % ( user_name, finished ),
} )
elif command == "RESOLVED":
ev.open()
resolved = ev.set_resolved( args or datetime.datetime.utcnow().strftime( "%Y-%m-%d %H:%M" ) )
ev.write()
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': "Event resolved time set by %s to: `%s`" % ( user_name, resolved ),
} )
elif command == "TIMELINE":
if not args:
return "You need to specify some text to add to the timeline..."
ev.open()
ev.add_timeline( time = datetime.datetime.utcnow().strftime( "%Y-%m-%d %H:%M" ), line = args )
ev.write()
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': "Timeline entry added by @%s:\n\n> %s" % ( user_name, args ),
} )
elif command == "INCIDENT":
ev.open()
flags = ev.toggle_incident()
ev.write()
index.open()
index.set_flags( flags.split() )
index.write()
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': "Flags set by @%s to: `%s`" % ( user_name, flags ),
} )
elif command == "DEGRADED":
ev.open()
flags = ev.toggle_degraded()
ev.write()
index.open()
index.set_flags( flags.split() )
index.write()
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': "Flags set by @%s to: `%s`" % ( user_name, flags ),
} )
elif command == "MAINTENANCE":
ev.open()
flags = ev.toggle_maintenance()
ev.write()
index.open()
index.set_flags( flags.split() )
index.write()
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': "Flags set by @%s to: `%s`" % ( user_name, flags ),
} )
elif command == "NOTICE":
ev.open()
flags = ev.toggle_notice()
ev.write()
index.open()
index.set_flags( flags.split() )
index.write()
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': "Flags set by @%s to: `%s`" % ( user_name, flags ),
} )
elif command == "OK":
ev.open()
flags = ev.toggle_ok()
ev.write()
index.open()
index.set_flags( flags.split() )
index.write()
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': "Flags set by @%s to: `%s`" % ( user_name, flags ),
} )
else:
return "WTF command?"
return ""
def mattermost_incident_start( description, team_id, user_name, user_id, channel_id ):
raw_incident_number = generate_raw_incident_number()
fi_number = raw_incident_number_to_public_incident_number( raw_incident_number )
channel_name = raw_incident_number_to_channel_name( raw_incident_number )
channel = app.mm.channels.create_channel( options = { "team_id": team_id,
"name": channel_name,
"display_name": fi_number,
"purpose": description,
"header": "",
"type": "O",
} )
# add user to the channel we just created
app.mm.client.make_request( 'post', '/channels/' + channel[ 'id' ] + '/members', options = { 'user_id': user_id,
} )
app.mm.posts.create_post( options = { 'channel_id': channel[ 'id' ],
'message': "Incident discussion channel for [%s](%s) created by @%s." % ( fi_number, raw_incident_number_to_url( raw_incident_number ), user_name ),
} )
now = datetime.datetime.utcnow().strftime( "%Y-%m-%d %H:%M" )
app.mm.posts.create_post( options = { 'channel_id': channel[ 'id' ],
'message': """\
# Incident Response Communication Procedure
The contents of this channel are strictly confidential.
## Public Announcement
1. adjust the name of the incident with **/purpose**
2. adjust the description of the incident with **/header**
3. choose a front page status (see below)
4. publish %(incident_url)s with **/incident PUBLISH**
## Status Updates
* add an update: **/incident TIMELINE** _text_
* set the event start: **/incident START** _%(now)s_ (UTC)
* set the event ETR: **/incident ETR** _%(now)s_ (UTC)
* set the event finished time: **/incident FINISHED** _%(now)s_ (UTC)
* set the event closure time: **/incident RESOLVED** _%(now)s_ (UTC)
Again, don't forget to **/incident PUBLISH** once making changes.
## Front Page Status
* **/incident INCIDENT**
* **/incident DEGRADED**
* **/incident MAINTENANCE**
* **/incident NOTICE**
* **/incident OK**
Again, after setting the headline status you must: **/incident PUBLISH**
## Good luck, Incident Commander @%(commander_name)s!""" % { "incident_url": raw_incident_number_to_url( raw_incident_number ),
"commander_name": user_name,
"now": now,
},
} )
app.mm.posts.create_post( options = { 'channel_id': channel_id,
'message': 'On behalf of @%s I have started incident ~%s.' % ( request.form[ 'user_name' ], channel_name, ),
} )
return ""
@app.route( '/mattermost/incident', methods = [ 'POST' ] )
def mattermost_incident():
# channel_name: testing
# command: /incident
# channel_id: o3ob1utim3nummoswjyt4174mo
# user_name: marek
# test_domain: faelix
# text: test
# team_id: 3woo8mbjrbb1in53xwdzi4ynqh
# user_id: ko3uxmend7ne8ger4uw9mxkt1o
# token: XXXXXXXXXXXXXXXXXXXXXXXXXX
if request.form[ 'token' ] in app.config[ 'MATTERMOST_COMMAND_TOKENS' ]:
if is_incident_channel_name( request.form[ 'channel_name' ] ):
text = request.form[ 'text' ].strip().split()
raw_incident_number = channel_name_to_raw_incident_number( request.form[ 'channel_name' ] )
return mattermost_incident_command( command = text[ 0 ].upper(),
args = " ".join( text[ 1: ] ),
channel_id = request.form[ 'channel_id' ],
user_name = request.form[ 'user_name' ],
raw_incident_number = raw_incident_number,
)
else:
return mattermost_incident_start( description = request.form[ 'text' ],
team_id = request.form[ 'team_id' ],
user_name = request.form[ 'user_name' ],
user_id = request.form[ 'user_id' ],
channel_id = request.form[ 'channel_id' ],
)
else:
return "Invalid command token."
def exec_to_message( cmdline ):
results = "#### `" + cmdline + "`:\n\n"
process = subprocess.Popen( cmdline, shell = True, cwd = app.config[ "STATUS_ROOT" ], stdout = subprocess.PIPE, stderr = subprocess.PIPE )
stdout = strip_progress( strip_ansi( process.stdout.read().decode( 'ascii', 'ignore' ) ) )
if stdout:
results += "\n\n`````````\n" + stdout + "`````````\n"
stderr = strip_progress( strip_ansi( process.stderr.read().decode( 'ascii', 'ignore' ) ) )
if stderr:
results += "\n\n`````````\n" + stderr + "`````````\n"
if not stdout and not stderr:
results += '_no output_\n'
return results
@app.route( '/mattermost/fih', methods = [ 'POST' ] )
7 years ago
def mattermost_fih():
# text: /incident PUBLISH
7 years ago
# file_ids:
# user_name: marek
# trigger_word: /incident
7 years ago
# channel_name: incident201806158693
# timestamp: 1529100914
# channel_id: zq61mk8ig7rmibtbyqf4babhfr
# team_id: 3woo8mbjrbb1in53xwdzi4ynqh
# token: nn54y4ozbtnzb8pri3c4gdhbce
# user_id: ko3uxmend7ne8ger4uw9mxkt1o
# team_domain: faelix
# post_id: 8cxn8yeyepyczcfbazdsd1bonw
if request.form[ 'token' ] in app.config[ 'MATTERMOST_WEBHOOK_TOKENS' ]:
pass
7 years ago
else:
return "Invalid command token."
@app.route( '/' )
def hello_world():
return 'Hello, World!'