diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 18db331..5719eed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - repository: [restic-auto, telegraf, piwigo-souvenirs, docker2mqtt, shairport-sync, snapserver] + repository: [docker2mqtt, gandi, mopidy, piwigo-souvenirs, restic-auto, shairport-sync, slack-eraser, snapserver, telegraf] steps: - name: Checkout diff --git a/gandi/Dockerfile b/gandi/Dockerfile new file mode 100644 index 0000000..862643b --- /dev/null +++ b/gandi/Dockerfile @@ -0,0 +1,8 @@ +FROM alpine:latest + +ADD updatedns.sh /usr/bin/updatedns.sh + +RUN apk add --no-cache curl jq \ + && chmod +x /usr/bin/updatedns.sh + +CMD /usr/bin/updatedns.sh diff --git a/gandi/README.md b/gandi/README.md new file mode 100644 index 0000000..0f798a9 --- /dev/null +++ b/gandi/README.md @@ -0,0 +1,20 @@ +Docker image to automatically update Gandi DNS with the current IP adress + +# Status +[![Github link](https://assets-cdn.github.com/favicon.ico)](https://github.com/napnap75/rpi-docker-images/) [![Docker hub link](https://www.docker.com/favicon.ico)](https://hub.docker.com/r/napnap75/rpi-gandi/) + + +# Content +This image is based [my own Alpine Linux base image](https://hub.docker.com/r/napnap75/rpi-alpine-base/). + +This image contains : +- the python Gandi CLI for the v4 version +- curl and jq to access Gandi REST API for the v5 version + +# Usage +Use this image to update a DNS record to the current IP of the host: `docker run -e GANDI_API_KEY="YOUR GANDI API KEY" -e GANDI_HOST="THE HOST NAME IN THE GANDI DOMAIN" -e GANDI_DOMAIN="YOUR GANDI DOMAIN" --name gandi napnap75/rpi-gandi:latest` +Use the following images : +- `napnap75/rpi-gandi:v4` if you use the v4 version of Gandi +- `napnap75/rpi-gandi:v5` or `napnap75/rpi-gandi:latest` if you use the v5 version of Gandi + +Every 5 minutes, the image will automatically check the current IP address of the host and, if necessary, update the DNS. diff --git a/gandi/updatedns.sh b/gandi/updatedns.sh new file mode 100644 index 0000000..1fd3631 --- /dev/null +++ b/gandi/updatedns.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# First check if the gandi cli is properly configured, otherwise configure it + +while true ; do + # Get my current IP + my_ip=$(curl -s https://api.ipify.org) + + # Get my currently registered IP + current_record=$(curl -s -H"X-Api-Key: $GANDI_API_KEY" https://dns.api.gandi.net/api/v5/domains/$GANDI_DOMAIN/records | jq -c '.[] | select(.rrset_name == "'$GANDI_HOST'") | select(.rrset_type == "A")') + current_ip=$(echo $current_record | jq -r '.rrset_values[0]') + + # Check if both IP addresses are correct + if [[ "$my_ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ && "$current_ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] ; then + # If they do not match, change it (and keep the TTL and TYPE) + if [[ "$my_ip" != "$current_ip" ]]; then + echo "Updating $GANDI_HOST.$GANDI_DOMAIN record with IP $my_ip" + current_ttl=$(echo $current_record | jq -r '.rrset_ttl') + curl -s -X PUT -H "Content-Type: application/json" -H "X-Api-Key: $GANDI_API_KEY" -d '{"rrset_ttl": '$current_ttl', "rrset_values":["'$my_ip'"]}' https://dns.api.gandi.net/api/v5/domains/$GANDI_DOMAIN/records/$GANDI_HOST/A + + # If the update was OK + if [[ $? == 0 ]] ; then + # Send a notification to Slack + if [[ "$SLACK_URL" != "" ]] ; then + curl -o /dev/null -s -X POST -d "payload={\"username\": \"gandi\", \"icon_emoji\": \":dart:\", \"text\": \"New IP $my_ip for host $GANDI_HOST.$GANDI_DOMAIN\"}" $SLACK_URL + fi + + # Send a notification to Healthchecks + if [[ "$HEALTHCHECKS_URL" != "" ]] ; then + curl -o /dev/null -s -m 10 --retry 5 $HEALTHCHECKS_URL + fi + fi + else + # Send a notification to Healthchecks + if [[ "$HEALTHCHECKS_URL" != "" ]] ; then + curl -o /dev/null -s -m 10 --retry 5 $HEALTHCHECKS_URL + fi + fi + fi + + # Wait 5 minutes + sleep 300 +done diff --git a/mopidy/Dockerfile b/mopidy/Dockerfile new file mode 100644 index 0000000..134f602 --- /dev/null +++ b/mopidy/Dockerfile @@ -0,0 +1,21 @@ +FROM alpine:edge AS builder + +ARG TARGETPLATFORM + +RUN apk add --no-cache curl \ + && DOWNLOAD_ARCH=$(echo ${TARGETPLATFORM} | sed "s#linux/arm/v6#arm#" | sed "s#linux/arm/v7#armhf#" | sed "s#linux/arm64#aarch64#" | sed "s#linux/amd64#amd64#") \ + && echo "DOWNLOAD_ARCH=${DOWNLOAD_ARCH}" \ + && S6_DOWNLOAD_URL=$(curl -s https://api.github.com/repos/just-containers/s6-overlay/releases/latest | grep "browser_download_url" | grep "s6-overlay-${DOWNLOAD_ARCH}.tar.gz\"" | cut -d\" -f4) \ + && curl --retry 3 -L -s -o /tmp/s6-overlay.tar.gz $S6_DOWNLOAD_URL \ + && mkdir /tmp/s6-overlay \ + && tar xzf /tmp/s6-overlay.tar.gz -C /tmp/s6-overlay + +FROM alpine:edge + +RUN apk add --no-cache py3-pip mopidy \ + && echo "30 7 * * * mopidy local scan" | crontab - + +COPY --from=builder /tmp/s6-overlay / +COPY etc /etc/ + +ENTRYPOINT ["/init"] diff --git a/mopidy/etc/cont-init.d/init-plugins.sh b/mopidy/etc/cont-init.d/init-plugins.sh new file mode 100755 index 0000000..2f2494a --- /dev/null +++ b/mopidy/etc/cont-init.d/init-plugins.sh @@ -0,0 +1,6 @@ +#!/usr/bin/with-contenv sh + +if [ "$MOPIDY_PLUGINS" != "" ]; then + echo "Installing plugins $MOPIDY_PLUGINS" + pip3 install $MOPIDY_PLUGINS +fi diff --git a/mopidy/etc/services.d/crond/run b/mopidy/etc/services.d/crond/run new file mode 100644 index 0000000..bd6581d --- /dev/null +++ b/mopidy/etc/services.d/crond/run @@ -0,0 +1,3 @@ +#!/bin/sh + +crond -f diff --git a/mopidy/etc/services.d/mopidy/run b/mopidy/etc/services.d/mopidy/run new file mode 100644 index 0000000..f7efa29 --- /dev/null +++ b/mopidy/etc/services.d/mopidy/run @@ -0,0 +1,3 @@ +#!/usr/bin/execlineb -P + +mopidy diff --git a/slack-eraser/Dockerfile b/slack-eraser/Dockerfile new file mode 100644 index 0000000..44b6b51 --- /dev/null +++ b/slack-eraser/Dockerfile @@ -0,0 +1,8 @@ +FROM python:alpine + +RUN apk add --no-cache bash curl \ + && pip3 install slacker + +ADD slack-eraser.py / + +CMD ["python", "/slack-eraser.py"] diff --git a/slack-eraser/defaults.ini b/slack-eraser/defaults.ini new file mode 100644 index 0000000..ab23f52 --- /dev/null +++ b/slack-eraser/defaults.ini @@ -0,0 +1,81 @@ +[DEFAULT] +SlackTocken = your-slack-api-tocken + +[my-first-eraser] +# Use 'duplicates' eraser to delete all messages that have already be found +Type = duplicates +# Channel where to look from +# Default: None (optional) +In = +# Username of the sender +# Default: None (optional) +From = +# Additionnal words to search for +# Default: None (optional) +Request = +# Sort by +# Default: timestamp +Sort = timestamp +# Sort order +# Default: desc +Order = desc +# Message count to search for +# Default: 100 +Count = 100 +# Regular expression to match the message with +# If none is provided, the full message text will be used as the key to find duplicates +# If one is provide, the matched subpart of the message text will be used +# If it contains parenthses to extract groups, the first group will be used +# Default: None (optional) +RegExp = ^Text message with the following key: ([0-9a-zA-Z_-]+) + +[my-second-eraser] +# Use 'alerts' eraser to delete alerts that have been closed and keep the open ones +Type = alerts +# Channel where to look from +# Default: None (optional) +In = +# Username of the sender +# Default: None (optional) +From = +# Additionnal words to search for +# Default: None (optional) +Request = +# Sort by +# Default: timestamp +Sort = timestamp +# Sort order +# Default: desc +Order = desc +# Message count to search for +# Default: 100 +Count = 100 +# Open alert status +# Default: OK +OpenStatus = OK +# Closed alert status +# Default: CRITICAL +ClosedStatus = CRITICAL +# Regular expression to match the message with +# Must contain at least two parenthses to extract groups, one for the alert level, the other one as the key to identify the alert +# Default: None (required) +RegExp = ^(OK|CRITICAL) : text message with the following key: ([^(]+) + +[my-third-eraser] +# Use 'older' eraser to delete messages that are older than the given number of days +Type = older +# Channel where to look from +# Default: None (optional) +In = +# Username of the sender +# Default: None (optional) +From = +# Additionnal words to search for +# Default: None (optional) +Request = +# Message count to search for +# Default: 100 +Count = 100 +# Number of days before which all messages should be erased +# Default: None (required) +OlderThan = 7 diff --git a/slack-eraser/slack-eraser.py b/slack-eraser/slack-eraser.py new file mode 100644 index 0000000..fac1e30 --- /dev/null +++ b/slack-eraser/slack-eraser.py @@ -0,0 +1,214 @@ +import argparse +import configparser +import time +import re +import ast + +from slacker import Slacker + +# Parse the arguments +parser = argparse.ArgumentParser(description='Utilityy to delete Slack message.') +parser.add_argument('file', help='The configuration file') +parser.add_argument('--verbose', '-v', action='count', help='Be verbose') +parser.add_argument('--dry-run', '-n', action='count', help='Do nothing (for test purpose)') +args = parser.parse_args() +if args.verbose != None : + print("Using args:", args) + +# Parse the configuration file +if args.verbose != None : + print("Parsing configuration file:", args.file) +config = configparser.ConfigParser() +config.read(args.file) + +# Open the connection to Slack +if args.verbose != None : + print("Opening Slack connection") +slack = Slacker(config['DEFAULT']['SlackTocken']) + +# Iterate over all sections in the config file +for sectionName in config.sections(): + section = config[sectionName] + eraserType = section.get('Type') + + searchRequest = section.get('Request', "") + searchFrom = section.get('From') + if searchFrom != None and " " not in searchFrom: + searchRequest += " from:" + searchFrom + searchIn = section.get('In') + if searchIn != None: + searchRequest += " in:" + searchIn + searchSort = section.get('Sort', "timestamp") + searchOrder = section.get('Order', "desc") + searchCount = section.get('Count', 100) + searchRE = section.get('RegExp') + regexp = None + if searchRE != None: + regexp = re.compile(searchRE) + + if eraserType == 'duplicates': + if args.verbose != None : + print("Deleting duplicates messages with request: \"", searchRequest, "\", sort:", searchSort, ", order:", searchOrder, "and count:", searchCount) + if searchRE != None: + print("And using regular expression:", searchRE) + + searchResponse = slack.search.messages(searchRequest, searchSort, searchOrder, None, searchCount) + messages = searchResponse.body['messages']['matches'] + seenMessages = [] + for message in messages: + text = message['text'] + if text == "": + try: + text = message['attachments'][0]['fallback'] + except KeyError as ex: + try: + text = message['attachments'][0]['text'] + except KeyError as ex: + if args.verbose != None and args.verbose > 1: + print("Message skipped because empty") + continue + if searchFrom != None and searchFrom != message['username']: + if args.verbose != None and args.verbose > 1: + print(text, "--> Skipped (From '", message['username'], "' != '", searchFrom, "')") + continue + if searchIn != None and searchIn != message['channel']['name']: + if args.verbose != None and args.verbose > 1: + print(text, "--> Skipped (In '", message['channel']['name'], "' != '", searchIn, "')") + continue + + key = text + if regexp != None: + match = regexp.match(text) + if match: + try: + key = match.group(1) + except IndexError as ex: + key = match.group(0) + if args.verbose != None and args.verbose > 1: + print(text, "using key:", key) + else: + if args.verbose != None and args.verbose > 1: + print(key, "--> Skipped (RegExp)") + continue + + if key in seenMessages: + if args.verbose != None and args.verbose > 1: + print(key, "--> Already seen") + if args.dry_run == None: + if args.verbose != None: + print("Deleting message timestamp", message['ts'], "in channel", message['channel']['name']) + slack.chat.delete(message['channel']['id'], message['ts']) + time.sleep(60/50) + else: + if args.verbose != None: + print("Should have deleted message timestamp", message['ts'], "in channel", message['channel']['name']) + else: + if args.verbose != None and args.verbose > 1: + print(key, "--> Not seen") + seenMessages.append(key) + elif eraserType == 'alerts': + closedStatus = ast.literal_eval(section.get('ClosedStatus', "['OK', 'Success']")) + openStatus = ast.literal_eval(section.get('OpenStatus', "['CRITICAL', 'Failure']")) + if args.verbose != None : + print("Deleting alerts messages with request: \"", searchRequest, "\", sort:", searchSort, ", order:", searchOrder, "and count:", searchCount) + print("And using regular expression:", searchRE, "with opening status:", openStatus, "and closing status:", closedStatus) + + searchResponse = slack.search.messages(searchRequest, searchSort, searchOrder, None, searchCount) + messages = searchResponse.body['messages']['matches'] + closedAlerts = [] + openedAlerts = [] + for message in messages: + text = message['text'] + if text is None or text == "": + try: + text = message['attachments'][0]['fallback'] + except KeyError as ex: + try: + text = message['attachments'][0]['text'] + except KeyError as ex: + if args.verbose != None and args.verbose > 1: + print("Message skipped because empty") + continue + if searchFrom != None and searchFrom != message['username']: + if args.verbose != None and args.verbose > 1: + print(text, "--> Skipped (From '", message['username'], "' != '", searchFrom, "')") + continue + if searchIn != None and searchIn != message['channel']['name']: + if args.verbose != None and args.verbose > 1: + print(text, "--> Skipped (In '", message['channel']['name'], "' != '", searchIn, "')") + continue + + status = None + match = regexp.match(text) + if match: + if match.group(1) in closedStatus: + closedAlerts.append(match.group(2)) + if args.verbose != None and args.verbose > 1: + print(text, "--> Alert '", match.group(2), "' closed") + elif match.group(1) in openStatus: + if match.group(2) in closedAlerts: + if args.verbose != None and args.verbose > 1: + print(text, "--> Alert '", match.group(2), "' already closed") + elif match.group(2) in openedAlerts: + if args.verbose != None and args.verbose > 1: + print(text, "--> Alert '", match.group(2), "' not closed but already seen") + else: + if args.verbose != None and args.verbose > 1: + print(text, "--> Alert '", match.group(2), "' not closed") + + openedAlerts.append(match.group(2)) + continue + elif match.group(2) in closedStatus: + closedAlerts.append(match.group(1)) + if args.verbose != None and args.verbose > 1: + print(text, "--> Alert '", match.group(1), "' closed") + elif match.group(2) in openStatus: + if match.group(1) in closedAlerts: + if args.verbose != None and args.verbose > 1: + print(text, "--> Alert '", match.group(1), "' already closed") + elif match.group(1) in openedAlerts: + if args.verbose != None and args.verbose > 1: + print(text, "--> Alert '", match.group(1), "' not closed but already seen") + else: + if args.verbose != None and args.verbose > 1: + print(text, "--> Alert '", match.group(1), "' not closed") + + openedAlerts.append(match.group(1)) + continue + else: + if args.verbose != None and args.verbose > 1: + print(text, "--> Skipped (Status not found)") + continue + else: + if args.verbose != None and args.verbose > 1: + print(text, "--> Skipped (Does not match regular expression)") + continue + + if args.dry_run == None: + if args.verbose != None: + print("Deleting message timestamp", message['ts'], "in channel", message['channel']['name']) + slack.chat.delete(message['channel']['id'], message['ts']) + time.sleep(60/50) + else: + if args.verbose != None: + print("Should have deleted message timestamp", message['ts'], "in channel", message['channel']['name']) + elif eraserType == 'older': + olderThan = int(ast.literal_eval(section.get('OlderThan'))) + if args.verbose != None : + print("Deleting messages older than ", olderThan, " days with request: \"", searchRequest, "\" and count:", searchCount) + + searchRequest = searchRequest + " before:" + time.strftime("%Y-%m-%d", time.gmtime(time.time()-olderThan*24*60*60)) + searchResponse = slack.search.messages(searchRequest, "timestamp", "desc", None, searchCount) + messages = searchResponse.body['messages']['matches'] + for message in messages: + if args.dry_run == None: + if args.verbose != None: + print("Deleting message timestamp", message['ts'], "in channel", message['channel']['name']) + slack.chat.delete(message['channel']['id'], message['ts']) + time.sleep(60/50) + else: + if args.verbose != None: + print("Should have deleted message timestamp", message['ts'], "in channel", message['channel']['name']) + else: + print("Unknown type ", eraserType, " in section", sectionName) +