mirror of
https://github.com/KevinMidboe/cloudflare-ddns.git
synced 2026-01-13 04:35:48 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 67e58fc8ee | |||
| f32267280a | |||
| 34e8ca8c95 | |||
| 7b5ddcec21 | |||
| c4d4755eeb | |||
| 8bb7f2e1ae | |||
| fe60206ef5 | |||
| 440f31fede | |||
| 11ceb88f1b | |||
| 776881775b | |||
| cdfb4aef5f | |||
| fde88fd655 | |||
| a9957a43b8 |
113
.drone.yml
113
.drone.yml
@@ -1,7 +1,25 @@
|
|||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: docker
|
type: docker
|
||||||
name: Build
|
name: Test
|
||||||
|
|
||||||
|
|
||||||
|
platform:
|
||||||
|
os: linux
|
||||||
|
arch: amd64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Run unit tests
|
||||||
|
image: python:3.10-alpine
|
||||||
|
commands:
|
||||||
|
- pip install -r requirements.txt
|
||||||
|
- pip install -r requirements-test.txt
|
||||||
|
- python3 -m unittest tests/test_*.py -v
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: Publish
|
||||||
|
|
||||||
platform:
|
platform:
|
||||||
os: linux
|
os: linux
|
||||||
@@ -12,23 +30,27 @@ steps:
|
|||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
settings:
|
settings:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
repo: ghcr.io/kevinmidboe/cloudflare-ddns
|
repo: ghcr.io/kevinmidboe/${DRONE_REPO_NAME}
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
username:
|
username:
|
||||||
from_secret: GITHUB_USERNAME
|
from_secret: GITHUB_USERNAME
|
||||||
password:
|
password:
|
||||||
from_secret: GITHUB_PASSWORD
|
from_secret: GHCR_UPLOAD_TOKEN
|
||||||
tags:
|
tags:
|
||||||
- latest
|
- latest
|
||||||
- ${DRONE_COMMIT_SHA}
|
- ${DRONE_COMMIT_SHA}
|
||||||
when:
|
|
||||||
event:
|
trigger:
|
||||||
include:
|
event:
|
||||||
- push
|
include:
|
||||||
exclude:
|
- push
|
||||||
- pull_request
|
exclude:
|
||||||
branch:
|
- pull_request
|
||||||
- main
|
branch:
|
||||||
|
- main
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- Test
|
||||||
|
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
@@ -40,26 +62,65 @@ platform:
|
|||||||
arch: amd64
|
arch: amd64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Prepare kubernetes environment
|
||||||
|
image: alpine/k8s:1.25.15
|
||||||
|
environment:
|
||||||
|
VAULT_TOKEN:
|
||||||
|
from_secret: VAULT_TOKEN
|
||||||
|
VAULT_HOST:
|
||||||
|
from_secret: VAULT_HOST
|
||||||
|
commands:
|
||||||
|
- mkdir -p /root/.kube
|
||||||
|
- echo "IMAGE=ghcr.io/kevinmidboe/${DRONE_REPO_NAME}:${DRONE_COMMIT_SHA}" > /root/.kube/.env
|
||||||
|
- echo "NAMESPACE=${DRONE_REPO_NAME}" >> /root/.kube/.env
|
||||||
|
- 'curl -s
|
||||||
|
-H "X-Vault-Token: $VAULT_TOKEN"
|
||||||
|
$VAULT_HOST/v1/schleppe/data/kazan/_infra
|
||||||
|
| jq -r ".data.data.KUBE_CONFIG" > /root/.kube/config'
|
||||||
|
- 'curl -s
|
||||||
|
-H "X-Vault-Token: $VAULT_TOKEN"
|
||||||
|
$VAULT_HOST/v1/schleppe/data/kazan/_infra
|
||||||
|
| jq -cr ".data.data | .[\"ghcr-login-secret\"] | @base64" > /root/.kube/dockerconfig.json'
|
||||||
|
- echo "DOCKER_CONFIG=$(cat /root/.kube/dockerconfig.json)" >> /root/.kube/.env
|
||||||
|
- 'curl -s
|
||||||
|
-H "X-Vault-Token: $VAULT_TOKEN"
|
||||||
|
$VAULT_HOST/v1/schleppe/data/kazan/${DRONE_REPO_NAME}
|
||||||
|
| jq -cr ".data.data | to_entries[] | .key + \"=\" + (.value | @base64)" >> /root/.kube/.env'
|
||||||
|
- sed -i '/^$/!s/^/export /' /root/.kube/.env
|
||||||
|
volumes:
|
||||||
|
- name: kube-config
|
||||||
|
path: /root/.kube
|
||||||
|
|
||||||
- name: Deploy to kubernetes
|
- name: Deploy to kubernetes
|
||||||
image: alpine/k8s:1.25.15
|
image: alpine/k8s:1.25.15
|
||||||
commands:
|
commands:
|
||||||
- mkdir -p /root/.kube
|
- source /root/.kube/.env > /dev/null 2>&1
|
||||||
- echo $KUBE_CONFIG | base64 -di > /root/.kube/config
|
|
||||||
- export IMAGE=ghcr.io/kevinmidboe/cloudflare-ddns:${DRONE_COMMIT_SHA}
|
|
||||||
- cat .kubernetes/*.yml
|
- cat .kubernetes/*.yml
|
||||||
| envsubst
|
| envsubst
|
||||||
| kubectl --kubeconfig=/root/.kube/config apply -f -
|
| kubectl --kubeconfig=/root/.kube/config apply -f -
|
||||||
environment:
|
volumes:
|
||||||
KUBE_CONFIG:
|
- name: kube-config
|
||||||
from_secret: KUBE_CONFIG
|
path: /root/.kube
|
||||||
when:
|
|
||||||
event:
|
trigger:
|
||||||
include:
|
event:
|
||||||
- push
|
include:
|
||||||
exclude:
|
- push
|
||||||
- pull_request
|
exclude:
|
||||||
branch:
|
- pull_request
|
||||||
- main
|
branch:
|
||||||
|
- main
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- Build
|
- Test
|
||||||
|
- Publish
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: kube-config
|
||||||
|
temp: {}
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: signature
|
||||||
|
hmac: 2fb50ffa037eb368bcf6d596ced4c0ef42cfde413781ee39dd42b5f695396132
|
||||||
|
|
||||||
|
...
|
||||||
|
|||||||
5
.env.example
Normal file
5
.env.example
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
API_KEY=
|
||||||
|
DDNS_ZONE=
|
||||||
|
|
||||||
|
SMS_API_KEY=
|
||||||
|
SMS_RECIPIENT=
|
||||||
16
.github/workflows/docker-image.yml
vendored
16
.github/workflows/docker-image.yml
vendored
@@ -1,16 +0,0 @@
|
|||||||
name: Docker Image CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "main" ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ "main" ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Build the Docker image
|
|
||||||
run: docker build . --file Dockerfile --tag cloudflare-ddns:$(github.sha)
|
|
||||||
12
.kubernetes/1-secret.yml
Normal file
12
.kubernetes/1-secret.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
type: Opaque
|
||||||
|
metadata:
|
||||||
|
name: secret-env-values
|
||||||
|
namespace: cloudflare-ddns
|
||||||
|
data:
|
||||||
|
DDNS_ZONE: ${DDNS_ZONE}
|
||||||
|
API_KEY: ${API_KEY}
|
||||||
|
SMS_API_KEY: ${SMS_API_KEY}
|
||||||
|
SMS_RECIPIENT: ${SMS_RECIPIENT}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
---
|
||||||
apiVersion: batch/v1
|
apiVersion: batch/v1
|
||||||
kind: CronJob
|
kind: CronJob
|
||||||
metadata:
|
metadata:
|
||||||
|
|||||||
9
.kubernetes/ghcr-token-secret.yml
Normal file
9
.kubernetes/ghcr-token-secret.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: ghcr-login-secret
|
||||||
|
namespace: ${NAMESPACE}
|
||||||
|
data:
|
||||||
|
.dockerconfigjson: ${DOCKER_CONFIG}
|
||||||
|
type: kubernetes.io/dockerconfigjson
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
FROM python:3.10-alpine
|
FROM python:3.10-alpine
|
||||||
|
|
||||||
COPY *.py ./
|
COPY requirements.txt .
|
||||||
|
COPY src/*.py ./
|
||||||
|
|
||||||
RUN pip install requests python-dotenv
|
RUN pip install -r requirements.txt
|
||||||
CMD python3 main.py
|
CMD python3 main.py
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -29,7 +29,7 @@ virtualenv
|
|||||||
source -p $(which python3) env/bin/activate
|
source -p $(which python3) env/bin/activate
|
||||||
|
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
python main.py
|
python src/main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## Kubernetes
|
## Kubernetes
|
||||||
@@ -46,8 +46,14 @@ metadata:
|
|||||||
namespace: cloudflare-ddns
|
namespace: cloudflare-ddns
|
||||||
type: Opaque
|
type: Opaque
|
||||||
data:
|
data:
|
||||||
API_KEY: BASE_64_ENCODED_CLOUDFLARE_API_KEY
|
API_KEY: CLOUDFLARE_API_KEY
|
||||||
DDNS_ZONE: BASE64_ENCODED_CLOUDFLARE_ZONE_ID
|
DDNS_ZONE: CLOUDFLARE_ZONE_ID
|
||||||
|
```
|
||||||
|
|
||||||
|
(Optional: receive a SMS from gateway API by appending to secrets data)
|
||||||
|
```yaml
|
||||||
|
SMS_API_KEY: GATEWAY_API_API_KEY
|
||||||
|
SMS_RECIPIENT: PHONE_NUMBER_TO_RECEIVE
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
49
main.py
49
main.py
@@ -1,49 +0,0 @@
|
|||||||
import os
|
|
||||||
import requests
|
|
||||||
from bulk_dns_update import updateAllZones, setAPIKey, getDDNSAddresszoneId
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from logger import logger
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
currentIP = None
|
|
||||||
DDNS_ZONE = os.getenv('DDNS_ZONE')
|
|
||||||
|
|
||||||
|
|
||||||
def publicAddress():
|
|
||||||
global currentIP
|
|
||||||
logger.info('Getting public IP from ifconfg.me...')
|
|
||||||
|
|
||||||
r = requests.get('https://ifconfig.me')
|
|
||||||
currentIP = r.text
|
|
||||||
logger.info('Public IP: {}'.format(currentIP))
|
|
||||||
|
|
||||||
|
|
||||||
def cloudflareDDNS():
|
|
||||||
logger.info('Checking IP recorded in Cloudflare...')
|
|
||||||
ddnsRecord = getDDNSAddresszoneId(DDNS_ZONE)
|
|
||||||
recordedIP = ddnsRecord['content']
|
|
||||||
logger.info('Found ddns recorded IP: {}'.format(recordedIP))
|
|
||||||
|
|
||||||
if currentIP != recordedIP:
|
|
||||||
logger.info('Public IP has changed, updating all A records.')
|
|
||||||
updateAllZones(recordedIP, currentIP)
|
|
||||||
else:
|
|
||||||
logger.info('is same, exiting')
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
apiKey = os.getenv('API_KEY')
|
|
||||||
if apiKey is None:
|
|
||||||
raise Exception('In .env file or environment set Cloudflare variable: API_KEY')
|
|
||||||
if DDNS_ZONE is None:
|
|
||||||
raise Exception('In .env file or environment; set Cloudflare zone where addr. points to current IP.')
|
|
||||||
|
|
||||||
setAPIKey(apiKey)
|
|
||||||
|
|
||||||
publicAddress()
|
|
||||||
cloudflareDDNS()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
1
requirements-test.txt
Normal file
1
requirements-test.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
responses==0.24.1
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
requests==2.31.0
|
||||||
|
python-dotenv==1.0.1
|
||||||
7
src/__init__.py
Normal file
7
src/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
PROJECT_PATH = os.getcwd()
|
||||||
|
SOURCE_PATH = os.path.join(
|
||||||
|
PROJECT_PATH, "src"
|
||||||
|
)
|
||||||
|
sys.path.append(SOURCE_PATH)
|
||||||
@@ -15,6 +15,7 @@ import requests
|
|||||||
from logger import logger
|
from logger import logger
|
||||||
|
|
||||||
API_KEY = ''
|
API_KEY = ''
|
||||||
|
DDNS_A_RECORD_NAME = 'addr'
|
||||||
|
|
||||||
|
|
||||||
def setAPIKey(apiKey):
|
def setAPIKey(apiKey):
|
||||||
@@ -63,6 +64,13 @@ def getZones():
|
|||||||
url = 'https://api.cloudflare.com/client/v4/zones'
|
url = 'https://api.cloudflare.com/client/v4/zones'
|
||||||
data = cloudflareRequest(url)
|
data = cloudflareRequest(url)
|
||||||
|
|
||||||
|
if 'success' not in data and 'errors' not in data:
|
||||||
|
logger.info(
|
||||||
|
"Unexpected cloudflare error when getting zones, no response!")
|
||||||
|
logger.info("data:" + str(data))
|
||||||
|
raise Exception(
|
||||||
|
'Unexpected Cloudflare error, missing response! Check logs.')
|
||||||
|
|
||||||
if data['success'] is False:
|
if data['success'] is False:
|
||||||
logger.info('Request to cloudflare was unsuccessful, error:')
|
logger.info('Request to cloudflare was unsuccessful, error:')
|
||||||
logger.info(data['errors'])
|
logger.info(data['errors'])
|
||||||
@@ -77,9 +85,17 @@ def getZones():
|
|||||||
|
|
||||||
|
|
||||||
def getRecordsForZone(zoneId):
|
def getRecordsForZone(zoneId):
|
||||||
url = 'https://api.cloudflare.com/client/v4/zones/{}/dns_records?type=A'.format(zoneId)
|
url = 'https://api.cloudflare.com/client/v4/zones/{}/dns_records?type=A'.format(
|
||||||
|
zoneId)
|
||||||
data = cloudflareRequest(url)
|
data = cloudflareRequest(url)
|
||||||
|
|
||||||
|
if 'success' not in data and 'errors' not in data:
|
||||||
|
logger.info(
|
||||||
|
"Unexpected cloudflare error when getting records, no response!")
|
||||||
|
logger.info("data:" + str(data))
|
||||||
|
raise Exception(
|
||||||
|
'Unexpected Cloudflare error, missing response! Check logs.')
|
||||||
|
|
||||||
if data['success'] is False:
|
if data['success'] is False:
|
||||||
logger.info('Request from cloudflare was unsuccessful, error:')
|
logger.info('Request from cloudflare was unsuccessful, error:')
|
||||||
logger.info(data['errors'])
|
logger.info(data['errors'])
|
||||||
@@ -95,17 +111,20 @@ def getRecordsForZone(zoneId):
|
|||||||
|
|
||||||
def getDDNSAddresszoneId(ddnsZone):
|
def getDDNSAddresszoneId(ddnsZone):
|
||||||
records = getRecordsForZone(ddnsZone)
|
records = getRecordsForZone(ddnsZone)
|
||||||
|
ddns_re = r"^{}\.".format(DDNS_A_RECORD_NAME)
|
||||||
|
|
||||||
for record in records:
|
for record in records:
|
||||||
if not re.match(r"^addr\.", record['name']):
|
if not re.match(ddns_re, record['name']):
|
||||||
continue
|
continue
|
||||||
return record
|
return record
|
||||||
|
|
||||||
raise Exception('No ddns record found for zone: {}'.format(ddnsZone))
|
raise Exception('No \'{}\' DDNS record found for zone: {}'.format(
|
||||||
|
DDNS_A_RECORD_NAME, ddnsZone))
|
||||||
|
|
||||||
|
|
||||||
def updateRecord(zoneId, recordId, name, newIP, ttl, proxied):
|
def updateRecord(zoneId, recordId, name, newIP, ttl, proxied):
|
||||||
url = 'https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}'.format(zoneId, recordId)
|
url = 'https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}'.format(
|
||||||
|
zoneId, recordId)
|
||||||
data = {
|
data = {
|
||||||
'type': 'A',
|
'type': 'A',
|
||||||
'name': name,
|
'name': name,
|
||||||
@@ -115,7 +134,8 @@ def updateRecord(zoneId, recordId, name, newIP, ttl, proxied):
|
|||||||
}
|
}
|
||||||
|
|
||||||
response = cloudflareUpdateRequest(url, data)
|
response = cloudflareUpdateRequest(url, data)
|
||||||
logger.info('\tRecord updated: {}'.format('✅' if response['success'] is True else '❌'))
|
logger.info('\tRecord updated: {}'.format(
|
||||||
|
'✅' if response['success'] is True else '❌'))
|
||||||
|
|
||||||
|
|
||||||
def getMatchingRecordsForZone(zoneId, oldIP):
|
def getMatchingRecordsForZone(zoneId, oldIP):
|
||||||
@@ -131,8 +151,10 @@ def updateZone(zone, oldIP, newIP):
|
|||||||
return
|
return
|
||||||
|
|
||||||
for record in records:
|
for record in records:
|
||||||
logger.info('\tRecord {}: {} -> {}'.format(record['name'], record['content'], newIP))
|
logger.info(
|
||||||
updateRecord(zone['id'], record['id'], record['name'], newIP, record['ttl'], record['proxied'])
|
'\tRecord {}: {} -> {}'.format(record['name'], record['content'], newIP))
|
||||||
|
updateRecord(zone['id'], record['id'], record['name'],
|
||||||
|
newIP, record['ttl'], record['proxied'])
|
||||||
|
|
||||||
|
|
||||||
def updateAllZones(oldIP, newIP):
|
def updateAllZones(oldIP, newIP):
|
||||||
71
src/main.py
Normal file
71
src/main.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
from bulk_dns_update import updateAllZones, setAPIKey, getDDNSAddresszoneId
|
||||||
|
from notify import notify
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from logger import logger
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
DDNS_ZONE = os.getenv('DDNS_ZONE')
|
||||||
|
|
||||||
|
|
||||||
|
def validIP(ipString):
|
||||||
|
ipRegex = r'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}' \
|
||||||
|
r'([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$'
|
||||||
|
return bool(re.search(ipRegex, str(ipString)))
|
||||||
|
|
||||||
|
|
||||||
|
def publicAddress():
|
||||||
|
logger.info('Getting public IP from ifconfg.me...')
|
||||||
|
|
||||||
|
r = requests.get('https://ifconfig.me')
|
||||||
|
if r.status_code != 200:
|
||||||
|
return
|
||||||
|
|
||||||
|
ip = r.text
|
||||||
|
if not validIP(ip):
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info('Public IP: {}'.format(ip))
|
||||||
|
return ip
|
||||||
|
|
||||||
|
|
||||||
|
def cloudflareDDNS():
|
||||||
|
logger.info('Checking IP recorded in Cloudflare...')
|
||||||
|
ddnsRecord = getDDNSAddresszoneId(DDNS_ZONE)
|
||||||
|
ip = ddnsRecord['content']
|
||||||
|
logger.info('Found ddns recorded IP: {}'.format(ip))
|
||||||
|
|
||||||
|
if not validIP(ip):
|
||||||
|
return
|
||||||
|
|
||||||
|
return ip
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
apiKey = os.getenv('API_KEY')
|
||||||
|
if apiKey is None:
|
||||||
|
raise Exception(
|
||||||
|
'In .env file or environment set Cloudflare variable: API_KEY')
|
||||||
|
if DDNS_ZONE is None:
|
||||||
|
raise Exception(
|
||||||
|
'In .env file or environment; set Cloudflare zone where addr. points to current IP.')
|
||||||
|
|
||||||
|
setAPIKey(apiKey)
|
||||||
|
|
||||||
|
currentIP = publicAddress()
|
||||||
|
recordedIP = cloudflareDDNS()
|
||||||
|
|
||||||
|
if currentIP == recordedIP or None in [currentIP, recordedIP]:
|
||||||
|
logger.info('is same, exiting')
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info('Public IP has changed, updating all A records.')
|
||||||
|
notify("IP changed to: {}. Updating all cloudflare zones!".format(currentIP))
|
||||||
|
updateAllZones(recordedIP, currentIP)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
42
src/notify.py
Normal file
42
src/notify.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import base64
|
||||||
|
import requests
|
||||||
|
from os import getenv
|
||||||
|
from json import dumps
|
||||||
|
from logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
def notify(message):
|
||||||
|
SMS_API_KEY = getenv('SMS_API_KEY')
|
||||||
|
SMS_RECIPIENT = getenv('SMS_RECIPIENT')
|
||||||
|
|
||||||
|
if not SMS_API_KEY:
|
||||||
|
logger.info("No SMS API key found, not notifying")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not SMS_RECIPIENT:
|
||||||
|
logger.info("No SMS recipient found, not notifying")
|
||||||
|
return
|
||||||
|
|
||||||
|
recipient = "47{}".format(SMS_RECIPIENT)
|
||||||
|
apiKey = base64.b64encode(SMS_API_KEY.encode("utf-8")).decode("utf-8")
|
||||||
|
|
||||||
|
logger.info("Notifying of IP change over SMS")
|
||||||
|
url = "https://gatewayapi.com/rest/mtsms"
|
||||||
|
payload = {
|
||||||
|
"sender": "Dynamic DNS",
|
||||||
|
"message": message,
|
||||||
|
"recipients": [{"msisdn": recipient}]
|
||||||
|
}
|
||||||
|
headers = {
|
||||||
|
"Host": "gatewayapi.com",
|
||||||
|
"Authorization": "Basic " + apiKey,
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
r = requests.post(url, data=dumps(payload), headers=headers)
|
||||||
|
response = r.json()
|
||||||
|
|
||||||
|
logger.info("Response from SMS api")
|
||||||
|
logger.info("Status: {} {}".format(str(r.status_code), str(r.reason)))
|
||||||
|
logger.info(response)
|
||||||
2
tests/__init__.py
Normal file
2
tests/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from src.logger import logger
|
||||||
|
logger.setLevel('WARNING')
|
||||||
80
tests/test_cloudflare_dns_record_ip.py
Normal file
80
tests/test_cloudflare_dns_record_ip.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import re
|
||||||
|
import unittest
|
||||||
|
import responses
|
||||||
|
from src.main import cloudflareDDNS
|
||||||
|
|
||||||
|
MOCK_IP = '44.208.147.61'
|
||||||
|
CLOUDFLARE_GET_RECORDS_URL = re.compile(
|
||||||
|
r"https\:\/\/api.cloudflare.com\/client\/v4\/zones\/\w*\/dns_records\?type\=A")
|
||||||
|
CLOUDFLARE_ADDR_RECORD_EXISTS_RESPONSE = {
|
||||||
|
'success': True,
|
||||||
|
'result': [{
|
||||||
|
'content': MOCK_IP,
|
||||||
|
'name': 'addr.',
|
||||||
|
'id': 'id',
|
||||||
|
'ttl': 86400,
|
||||||
|
'proxied': True
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
CLOUDFLARE_ADDR_RECORD_NONEXISTANT_RESPONSE = {
|
||||||
|
'success': True,
|
||||||
|
'result': [{
|
||||||
|
'content': MOCK_IP,
|
||||||
|
'name': None,
|
||||||
|
'id': 'id',
|
||||||
|
'ttl': 86400,
|
||||||
|
'proxied': True
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
CLOUDFLARE_500_RESPONSE = {
|
||||||
|
'success': False,
|
||||||
|
'errors': 'someerror'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestCloudflareDNSRecordIP(unittest.TestCase):
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_successfull_response(self):
|
||||||
|
responses.add(responses.GET, CLOUDFLARE_GET_RECORDS_URL,
|
||||||
|
json=CLOUDFLARE_ADDR_RECORD_EXISTS_RESPONSE, status=200)
|
||||||
|
|
||||||
|
ip = cloudflareDDNS()
|
||||||
|
|
||||||
|
self.assertEqual(MOCK_IP, ip)
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_addr_record_exists(self):
|
||||||
|
responses.add(responses.GET, CLOUDFLARE_GET_RECORDS_URL,
|
||||||
|
json=CLOUDFLARE_ADDR_RECORD_NONEXISTANT_RESPONSE,
|
||||||
|
status=200)
|
||||||
|
|
||||||
|
self.assertRaises(Exception, cloudflareDDNS)
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_cloudflare_500_response(self):
|
||||||
|
responses.add(responses.GET, CLOUDFLARE_GET_RECORDS_URL,
|
||||||
|
json=CLOUDFLARE_500_RESPONSE,
|
||||||
|
status=500)
|
||||||
|
|
||||||
|
self.assertRaises(Exception, cloudflareDDNS)
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_cloudflare_empty_200_response(self):
|
||||||
|
responses.add(responses.GET, CLOUDFLARE_GET_RECORDS_URL,
|
||||||
|
json={},
|
||||||
|
status=500)
|
||||||
|
|
||||||
|
self.assertRaises(Exception, cloudflareDDNS)
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_cloudflare_empty_500_response(self):
|
||||||
|
responses.add(responses.GET, CLOUDFLARE_GET_RECORDS_URL,
|
||||||
|
json={},
|
||||||
|
status=500)
|
||||||
|
|
||||||
|
self.assertRaises(Exception, cloudflareDDNS)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
33
tests/test_ip_address.py
Normal file
33
tests/test_ip_address.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import unittest
|
||||||
|
from src.main import validIP
|
||||||
|
|
||||||
|
MOCK_IP = "44.208.147.61"
|
||||||
|
|
||||||
|
|
||||||
|
class TestIPAddress(unittest.TestCase):
|
||||||
|
def test_valid_ip(self):
|
||||||
|
self.assertTrue(validIP(MOCK_IP))
|
||||||
|
|
||||||
|
def test_invalid_ip(self):
|
||||||
|
ip = "256.0.0.1"
|
||||||
|
self.assertFalse(validIP(ip))
|
||||||
|
|
||||||
|
def test_invalid_format(self):
|
||||||
|
ip = "192.168.1"
|
||||||
|
self.assertFalse(validIP(ip))
|
||||||
|
|
||||||
|
def test_empty_string(self):
|
||||||
|
ip = ""
|
||||||
|
self.assertFalse(validIP(ip))
|
||||||
|
|
||||||
|
def test_error_looking_string(self):
|
||||||
|
ip = "upstream connect error or disconnect/reset before headers. reset reason: connection timeout."
|
||||||
|
self.assertFalse(validIP(ip))
|
||||||
|
|
||||||
|
def test_none(self):
|
||||||
|
ip = None
|
||||||
|
self.assertFalse(validIP(ip))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
40
tests/test_public_ip.py
Normal file
40
tests/test_public_ip.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import unittest
|
||||||
|
import responses
|
||||||
|
from src.main import publicAddress
|
||||||
|
|
||||||
|
MOCK_IP = '44.208.147.61'
|
||||||
|
MOCK_TIMEOUT = 'upstream connect error or disconnect/reset before headers. reset reason: connection timeout.'
|
||||||
|
|
||||||
|
|
||||||
|
class TestPublicAddress(unittest.TestCase):
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_successfull_response(self):
|
||||||
|
responses.add(responses.GET, 'https://ifconfig.me',
|
||||||
|
body=MOCK_IP, status=200)
|
||||||
|
|
||||||
|
ip = publicAddress()
|
||||||
|
|
||||||
|
self.assertEqual(MOCK_IP, ip)
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_timeout_response(self):
|
||||||
|
responses.add(responses.GET, 'https://ifconfig.me',
|
||||||
|
body=MOCK_TIMEOUT, status=500)
|
||||||
|
|
||||||
|
ip = publicAddress()
|
||||||
|
|
||||||
|
self.assertIsNone(ip)
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_mangled_response(self):
|
||||||
|
responses.add(responses.GET, 'https://ifconfig.me',
|
||||||
|
body='123.22', status=200)
|
||||||
|
|
||||||
|
ip = publicAddress()
|
||||||
|
|
||||||
|
self.assertIsNone(ip)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user