I’ve been working towards making things simpler when managing distributed resources at work. And since we spend most of our day in the chat room (was Slack, now Mattermost) I thought it’s best to get started with ChatOps
It’s just a fancy word for doing stuff right from the chat window. And there’s so much one can do, especially with simple Slash Commands.
Here’s a lambda function I setup yesterday for invalidating CloudFront distributions.
from time import time
import boto3
import json
import os
import re
EXPECTED_TOKEN = os.environ['mmToken']
ALLOWED_USERS = re.split('[, ]', os.environ['allowedUsers'])
DISTRIBUTIONS = {
'site-name': 'DISTRIBUTIONID',
'another.site': 'DISTRIBUTIONID05'
}
def parse_command_text(command_text):
pattern = r"({})\s+(.*)".format('|'.join(DISTRIBUTIONS.keys()))
m = re.match(pattern, command_text)
if m:
return { 'site': m.group(1), 'path': path}
else:
return False
def lambda_handler(event, context):
# Parse the request
try:
request_data = event["queryStringParameters"]
except:
return {
"statusCode": 400,
"headers": {"Content-Type": "application/json"},
"body": '{ "message": "Use GET for setting up mattermost slash command" }'
}
# Check the token matches.
if request_data.get("token", "") != EXPECTED_TOKEN:
print('Wrong Token!')
return {
"statusCode": 401,
"headers": {"Content-Type": "application/json"},
"body": '{ "message": "Mattermost token does not match" }'
}
# Check the user is allowed to run the command
if request_data.get("user_name", "") not in ALLOWED_USERS:
print('Wrong User! {} not in {}'.format(request_data['user_name'], ALLOWED_USERS))
return {
"statusCode": 401,
"headers": {"Content-Type": "application/json"},
"body": '{ "message": "User not allowed to perform action" }'
}
# parse the command
command_text = request_data.get("text", "")
if not command_text:
print('Nothing to do, bailing out')
return {
"statusCode": 404,
"headers": {"Content-Type": "application/json"},
"body": '{ "message": "No command text sent" }'
}
parts = parse_command_text(command_text)
if not parts:
print('Bad formatting - command: {}'.format(command_text))
return {
"statusCode": 402,
"headers": {"Content-Type": "application/json"},
"body": '{ "message": "Wrong pattern" }'
}
# Do the actual work
cf_client = boto3.client('cloudfront')
# Invalidate
boto_response = cf_client.create_invalidation(
DistributionId=DISTRIBUTIONS[parts['site']],
InvalidationBatch={
'Paths': {
'Quantity': len(parts['path']),
'Items': parts['path']
},
'CallerReference': str(time()).replace(".", "")
}
)['Invalidation']
# Build the response message text.
text = """##### Executing invalidation
| Key | Info |
| --- | ---- |
| Site | {} |
| Path | {} |
| ID | {} |
| Status | {} |""".format(
parts['site'],
parts['path'],
boto_response['Id'],
boto_response['Status']
)
# Build the response object.
response = {
"response_type": "in_channel",
"text": text,
}
# Return the response as JSON
return {
"body": json.dumps(response),
"headers": {"Content-Type": "application/json"},
"statusCode": 200,
}
Note that you need to hook that up with an API Gateway in AWS. Once that’s done, you will have a URL endpoint ready for deployment.
Next, I created the slash command in mattermost with the following:
slash command configuration
That’s pretty much it. Rinse and repeat for a different command, different usage.
On my list next is to have more interaction with the user in mattermost per https://docs.mattermost.com/developer/interactive-messages.html Weekend Project, Yay!
I use Tilix as my main terminal app on Ubuntu. I like it mainly because it’s easy to use and shows multiple sessions in tiles and tabs, so it’s easy to switch between multiple servers without too much fuss
Tilix is also pretty customizable. I just needed to have the “profile” automatically switch according to the server/machine I switch to. This can be done according to the documentation per https://gnunn1.github.io/tilix-web/manual/profileswitch/ but I wanted something configured locally, not sent to the remote system.
So I just added a function that wraps my ssh command. Here’s how it looks in my .bashrc
This sets up the hostname of machine you’re logging into as the title. That’s your trigger. What remains is to create a profile and assign that profile to automatically switch when the trigger (hostname:directory, user can also be added) is set.
Go to Profiles > Edit Profile > Advanced (tab) and under Match add the hostname.
That’s about it. I’m going to add a new profile with a RED background now for my production machines! too much?
Update (Jan 8, 2020)
I noticed that I have also added a Match for my localhost hostname in the Default profile, so that the profile would revert to that once I logged off a remote host.
Another thing I needed was to create a special trigger if I wanted to use a wildcard match for hostnames (ie. if I wanted to switch profile on all my AWS instances via Session Manager). And match that special hostname (aws-instance below for example). Here’s my ssh bash function now:
ssh () {
SSHAPP=/usr/bin/ssh
ARGS=$@
if [[ $ARGS == i-* ]]
then
echo "switching to AWS instance"
printf "\033]7;file://%s/\007" "aws-instance"
else
echo "switching to $ARGS"
printf "\033]7;file://%s/\007" "$ARGS"
fi
$SSHAPP $ARGS
}
This is something you will have to deal with at one time or another after managing AWS IAM users for a while. Basically, it’s straight forward with the following code:
Get a list of users
for each user get their access key IDs
(optional) pipe to grep for checking a specific ID
And here’s the `aws-cli` code
for user in $(aws iam list-users --output text --query 'Users[*].UserName'); do
aws iam list-access-keys --user $user --output text;
done
# or
Q_ACCESSKEY=AKIA*************
for user in $(aws iam list-users --output text --query 'Users[*].UserName'); do
aws iam list-access-keys --user $user --output text | grep $Q_ACCESSKEY
done