Browse Source

initial commit...

tags/v0.1
Rune Olsen 11 months ago
commit
af65caec71
6 changed files with 652 additions and 0 deletions
  1. +135
    -0
      .gitignore
  2. +31
    -0
      README.md
  3. BIN
     
  4. BIN
     
  5. +222
    -0
      main.py
  6. +264
    -0
      utils.py

+ 135
- 0
.gitignore View File

@@ -0,0 +1,135 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
*.sh
.idea/
*.tar.*
*.desktop
dyndns.png


# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

+ 31
- 0
README.md View File

@@ -0,0 +1,31 @@
# DO DynDNS
DO DynDNS is a small Python app that:
* Lets you add DNS records to your DigitalOcean Network service.
* Adds records to your DigitalOcean Network service.
* Lists all sub domains on your "Dynamic DNS" domain
### Installation
* Download the _tar.gz_ file from the [relase page](https://gitlab.no/rune/DO_DynDNS/releases)
* Untar it and run `./install.sh` (remember to reas/review the install.sh with a text editor)
* The install.sh command will copy the file to `/usr/bin` and create a .desktop file in `/usr/share/applications/`
* Run by executing dyndns from terminal or by searching for it in your DM.

The first run will create two files:
* `~/.config/dyndns/config.ini`
* `~/.config/dyndns/dyndns.log`

You will need edit the config.ini file in your favourite editor.

```
[DYNDNS]

'api_key': '<API KEY> from Digital Ocean at https://cloud.digitalocean.com/api_access',
'baseuri': 'example.tld',
'logfile': 'dyndns.log'
```

The API key you need to get from you DigitalOcean account. The baseuri are the top level domain you want to use as the base for your dynamic DNS solution.

The logfile can be named anything, but the `~/.config/dyndns/dyndns.log` was created for you. So you don't need to change that.

The `.desktop` file will look for an icon "dyndns.png" in `~/.config/dyndns/`. You can copy your own icon to that folder and name it "dyndns.png" or you can copy the icon in the un-zipped folder. The application itself will also look for "dyndns.png" in the folder `~/.config/dyndns/`

BIN
View File


BIN
View File


+ 222
- 0
main.py View File

@@ -0,0 +1,222 @@
# !/usr/bin/env python3
from os import path
import PySimpleGUI as sg
import utils as dyndns
import sys
bundle_dir = getattr(sys, '_MEIPASS', path.abspath(path.dirname(__file__)))


def add_new_domain():
add_new = True
layout4 = [[sg.Text('This will add a new subdomain to you DO networkin resouces '
'\nand update your config.ini file.\n', font=(any,11))
],
[sg.InputText('', font=(any, 12), key='domainname',size=(20,1)),
sg.Text('.'+dyndns.domain_name, font=(any, 12))],
[sg.Text('\n\n')],
[sg.Button('Exit', key='Exit'), sg.Button('Add', key='Add')]
]

window4 = sg.Window('Add new domain to '+dyndns.domain_name, layout4)
while add_new:
event4, values4 = window4.Read()
if event4 == 'Add':
domain = values4['domainname']
result = dyndns.addnewdomin(domain)
if result:
sg.popup_ok('The domain ' + domain + ' added to config.ini', title='SUCCESS added domain ' + domain,
font=(any, 12), )
else:
sg.popup_ok('The domain ' + domain + ' was NOT added to config.ini', title='FAILURE in adding ' +
domain, font=(any, 12), )

if event4 is None or event4 == 'Exit':
window4.close()
add_new = False


def remove_domain_from_dyndns():
remove_from_ini = True
domains = dyndns.domainnames()
layout5 = [
[sg.Text('Select domain ', font=(any, 12)),
sg.Combo(default_value=domains[0], values=domains, size=(20, 1), enable_events=True, key='-domain5-',
font=(any, 12)),
],
[sg.Button('Exit', key='Exit'), sg.Button('Remove', key='Remove')]
]
window5 = sg.Window('Remove domain from DO DynDns', layout5)
while remove_from_ini:
event5, values5 = window5.Read()
if event5 == 'Remove':
domain = values5['-domain5-']
result = dyndns.remove_from_config(domain)
if result:
sg.popup_ok('The domain ' + domain + ' removed from config.ini', title='SUCCESS removed domain ' +
domain, font=(any, 12), )
else:
sg.popup_ok('The domain ' + domain + ' was NOT removed to config.ini', title='FAILURE in removing ' +
domain, font=(any, 12), )

if event5 is None or event5 == 'Exit':
window5.close()
remove_from_ini = False


def remove_domain_from_do():
None


def add_existing_domain():
add_existing_active = True
domains = dyndns.list_domains_to_array()
layout3 = [
[sg.Text('Select domain ', font=(any, 12)),
sg.Combo(default_value=domains[0][0], values=domains[0], size=(20, 1), enable_events=True, key='-domain3-',
font=(any, 12)),
],
[sg.Button('Exit', key='Exit'), sg.Button('Add', key='Add')]
]
window3 = sg.Window('Add existing domain to ' + dyndns.domain_name, layout3)
while add_existing_active:
event3, values3 = window3.Read()
if event3 == 'Add':
domain = values3['-domain3-']
number = domains[0].index(domain)
result = dyndns.add_existing_domain(domains[0][number], domains[1][number])
if result:
sg.popup_ok ('The domain ' + domain + ' added to config.ini', title='SUCCESS added domain ' + domain,
font=(any, 12),)
else:
sg.popup_ok('The domain ' + domain + ' was NOT added to config.ini', title='FAILURE in adding ' +
domain, font=(any, 12), )

if event3 is None or event3 == 'Exit':
window3.close()
add_existing_active = False


def list_domains():
domains_active = True
# window 2 layout - note - must be "new" every time a window is created
layout2 = [
[sg.Text(''.join(dyndns.list_all_domains_to_array()), font=(any, 12))],
[sg.Button('Exit')]
]
window2 = sg.Window('All domains on ' + dyndns.domain_name, layout2)
# Read window 2's events. Must use timeout of 0
while domains_active:
event2, values2 = window2.Read()
if event2 is None or event2 == 'Exit':
window2.close()
domains_active = False


def main_window():
subdomains = dyndns.domainnames()
localip = dyndns.local_ip4
# sg.change_look_and_feel('LightGreen')
sg.change_look_and_feel('DarkGrey')
sg.set_options(element_padding=(0, 0))
local_ip4_frame = [
[sg.T(localip, font=('Helvetica', 15, 'normal'), auto_size_text=True,key='localip')]
]
remote_ip4_frame = [
[sg.T(dyndns.getcurrentremoteip(subdomains[0]), font=('Helvetica', 15, 'normal'), auto_size_text=True,key='remoteip')]
]
# ------ Menu Definition ------ #
menu_def = [['&File', ['&Quit']],
['&Tools', ['Add &new domain',
'Add &existing domain',
'&Remove domain',
'&List domains',
'---',
'&Refresh']],
['&Help', ['&Help',
'&About']]
]

# ------ GUI Defintion ------ #
layout = [
[sg.Menu(menu_def, tearoff=False, pad=(20, 1))],
[sg.Text('Select domain ',font=(any,12)),
sg.Combo(default_value=subdomains[0], values=subdomains, size=(20, 1), enable_events=True, key='-domain-',
font=(any,12)),
sg.Frame('Local IP', local_ip4_frame, font='Any 14', title_color='black', element_justification='center',
pad=((103, 3),3))],
[sg.Frame('Remote IP', remote_ip4_frame, font='Any 14', title_color='black',
element_justification='center', pad=((450, 3), 3))],

# [sg.Text(dyndns.local_ip4)]
]

window = sg.Window("Digital Ocean DynDNS",
layout,
default_element_size=(12, 1),
auto_size_text=False,
auto_size_buttons=False,
default_button_element_size=(12, 1),
size=(700, 200),
icon=dyndns.filepath+'dyndns.png')

domains_active = False
add_existing_active = False
add_new = False
remove_from_ini = False
# ------ Loop & Process button menu choices ------ #
while True:
event, values = window.read()
if event in (None, 'Quit'):
return
if event == '-domain-':
currentdomain = values['-domain-']
remoteip = dyndns.getcurrentremoteip(values['-domain-'])
window.element('remoteip').Update(remoteip)
if remoteip != localip:
update = sg.popup_ok_cancel("The IP address (" + remoteip + ") for " + currentdomain +
' is different than your local IP address (' + localip + ').'
'\n\nDo you want to update the remote IP?\n\n',
title='Update remote IP',font=(any, 12),)
if update == 'OK':
result = dyndns.updateip(localip, currentdomain)
if result[0]:
window.element('remoteip').Update(localip)
else:
sg.popup_ok(result[1], title='Error updating IP!', font=(any, 12))
else:
continue

# print('Event = ', event)
# ------ Process menu choices ------ #
if event == 'About':
window.disappear()
sg.popup('DO DynDNS is created with Python and PySimpleGUI.\n\nAuthor: Rune Olsen\n\n\n\n',
title='About Digital Ocean DynDNS - v0.1', grab_anywhere=True,font=(any, 11))
window.reappear()

if event == 'Refresh':
remoteip = dyndns.getcurrentremoteip(values['-domain-'])
localip = dyndns.refreshlocalip()
window.element('remoteip').Update(remoteip)
window.element('localip').Update(localip)
window.element('-domain-').Update(values=dyndns.refresh_domainnames_from_file())

if event == 'List domains' and not domains_active: # only run if not already showing List domains
list_domains()

if event == 'Add existing domain' and not add_existing_active: # only run if not already showing Add existing
add_existing_domain()
window.element('-domain-').Update(values=dyndns.refresh_domainnames_from_file())

if event == 'Add new domain' and not add_new: # only run if not already showing Add new
add_new_domain()
window.element('-domain-').Update(values=dyndns.refresh_domainnames_from_file())

if event == 'Remove domain' and not remove_from_ini: # only run if not already showing remove domain
remove_domain_from_dyndns()
window.element('-domain-').Update(values=dyndns.refresh_domainnames_from_file())

window.close()


main_window()

+ 264
- 0
utils.py View File

@@ -0,0 +1,264 @@
#!/usr/bin/python3
import urllib.request
import configparser
import json
import logging
import argparse
import requests
import os
import time
import sys
import PySimpleGUI as sg

# Config stuff

# filepath = os.path.dirname(os.path.abspath(__file__))
linuxfilepath = '~/.config/dyndns/'
filepath = os.path.expanduser(linuxfilepath)
config = configparser.ConfigParser()


def addemptyconfig():
if not os.path.exists(filepath):
os.makedirs(filepath)
if not os.path.exists(filepath+'dyndns.log'):
open(filepath+'dyndns.log', 'a').close()
section = '[DYNDNS]'
section = section.strip("[']")
config[section] = {
'api_key': '<API KEY> from Digital Ocean at https://cloud.digitalocean.com/api_access',
'baseuri': 'example.tld',
'logfile': 'dyndns.log'
}
with open(filepath+'config.ini', 'w') as f:
config.write(f)


try:
config.read(filepath+'config.ini')
api = config['DYNDNS']['api_key']
domain_name = config['DYNDNS']['baseuri']
if not os.path.exists(filepath+config['DYNDNS']['logfile']):
open(filepath+config['DYNDNS']['logfile'], 'a').close()
logging.basicConfig(filename=filepath+config['DYNDNS']['logfile'], level=logging.INFO)
except KeyError:
addemptyconfig()
logging.basicConfig(filename=filepath+'dyndns.log', level=logging.INFO)
logging.error(time.strftime("%Y-%m-%d %H:%M") + ' Missing config.ini. Added to '+filepath)
sg.popup('Woops!\n\nMissing config.ini file!\n\n'
'An empty file has been created in' + filepath + ' as config.ini\n\n'
'please edit this file before starting the application again!',
title='About Digital Ocean DynDNS - v0.1', grab_anywhere=True,font=(any, 12))
sys.exit()


local_ip4 = urllib.request.urlopen("http://ip4.iurl.no").read().decode('utf-8')
local_ip4_nice = local_ip4 + ' '

# The program


def refreshlocalip():
return urllib.request.urlopen("http://ip4.iurl.no").read().decode('utf-8')


def refresh_domainnames_from_file():
config = None
config = configparser.ConfigParser()
config.read(filepath + 'config.ini')
api = config['DYNDNS']['api_key']
domain_name = config['DYNDNS']['baseuri']
names = []
for section in config.sections():
if not config.has_option(section, 'subdomainid'):
continue
for name in config.items(section):
names.append(section)
return names


def domainnames():
names = []
for section in config.sections():
if not config.has_option(section, 'subdomainid'):
continue
for name in config.items(section):
names.append(section)
return names


def updateip(ip, domainname):
current = config.items(section=domainname)
domain_id = current[0][1]
data = {'data': ip}
headers = {'Authorization': 'Bearer ' + api, "Content-Type": "application/json"}
response = requests.put('https://api.digitalocean.com/v2/domains/'+domain_name+'/records/' + domain_id, data=json.dumps(data), headers=headers)
if str(response) == '<Response [200]>':
result = [True, response]
logging.info(time.strftime("%Y-%m-%d %H:%M") + ' - Success! Domain %s updated with IP: %s ',
domainname, local_ip4)
else:
result = [False, response]
logging.error('Failure! ' + str(response))

return result


def getremoteip():
count = 0
for section in config.sections():
if not config.has_option(section, 'subdomainid'):
continue
for name, value in config.items(section):
count = count + 1
req = urllib.request.Request('https://api.digitalocean.com/v2/domains/' + domain_name + '/records/' +
value)
req.add_header('Content-Type', 'application/json')
req.add_header('Authorization', 'Bearer ' + api)
current = urllib.request.urlopen(req)
remote = current.read().decode('utf-8')
remoteData = json.loads(remote)
remoteIP4 = remoteData['domain_record']['data']
if count > 0:
break

return remoteIP4


def createnewdomain(name, ip):
data = {'name': name,
'data': ip,
'type': "A",
'ttl': 3600
}
headers = {'Authorization': 'Bearer ' + api, "Content-Type": "application/json"}
response = requests.post('https://api.digitalocean.com/v2/domains/' + domain_name + '/records',
data=json.dumps(data), headers=headers)
if str(response) == '<Response [201]>':
return response.json()
else:
return 'Fail'


def list_all_domains_to_array():
LocalDonains = []
domains = []
existing_in_ini = domainnames()
for section in config.sections():
if not config.has_option(section, 'subdomainid'):
continue
for subdomainid, value in config.items(section, 'subdomainid'):
LocalDonains.append(value)
req = urllib.request.Request('https://api.digitalocean.com/v2/domains/' + domain_name + '/records/')
req.add_header('Content-Type', 'application/json')
req.add_header('Authorization', 'Bearer ' + api)
current = urllib.request.urlopen(req)
remote = current.read().decode('utf-8')
remoteData = json.loads(remote)
domains.append('Domain\tID\t\tDynDNS\t\tIP\n')
for k in remoteData["domain_records"]:
if k['name'] != '@':
if str(k['data']) == local_ip4 or k['name'] in existing_in_ini:
if str(k['data']) != local_ip4:
domains.append(k['name'] + '\t' + str(k['id']) + '\tYes\t\t' + k['data'] + ' Check IP!\n')
else:
domains.append(k['name'] + '\t' + str(k['id']) + '\tYes\t\t' + k['data'] + '\n')
else:
domains.append(k['name'] + '\t' + str(k['id']) + '\tNo\t\t' + k['data'] + '\n')

return domains


def list_domains_to_array():
domain_names = []
domain_ids = []
for section in config.sections():
if not config.has_option(section, 'subdomainid'):
continue
# for subdomainid, value in config.items(section, 'subdomainid'):
# LocalDonains.append(value)
req = urllib.request.Request('https://api.digitalocean.com/v2/domains/' + domain_name + '/records/')
req.add_header('Content-Type', 'application/json')
req.add_header('Authorization', 'Bearer ' + api)
current = urllib.request.urlopen(req)
remote = current.read().decode('utf-8')
remoteData = json.loads(remote)
for k in remoteData["domain_records"]:
if k['name'] != '@':
if str(k['data']) != local_ip4 and str(k['name']) not in config.sections():
domain_names.append(k['name'])
domain_ids.append(str(k['id']))

return domain_names, domain_ids


def add_existing_domain(name, domainid):
section = str(name)
config[section] = {
'subdomainid': str(domainid)
}
with open(filepath+'config.ini', 'w') as f:
try:
config.write(f)
result = True
except:
result = False

if result:
logging.info(time.strftime("%Y-%m-%d %H:%M")+'Success! Domain %s added with ID: %s', name, domainid)
else:
logging.error(time.strftime("%Y-%m-%d %H:%M")+'Failure! Domain %s NOT added.', name)

return result


def addnewdomin(newdata):
currentlocalip = urllib.request.urlopen("http://ip4.iurl.no").read().decode('utf-8')
response = createnewdomain(newdata, currentlocalip)
if response is not 'Fail':
logging.info(time.strftime("%Y-%m-%d %H:%M")+'Success! Domain %s added to DigitalOcean (%s) with ID: %s', newdata, domain_name,
response['domain_record']['id'])
domainid = str(response['domain_record']['id'])
section = str(newdata)
section = section.strip("[']")
config[section] = {
'subdomainid': str(domainid)
}
with open(filepath+'config.ini', 'w') as f:
config.write(f)
result = True
else:
result = False
logging.error(time.strftime("%Y-%m-%d %H:%M")+'Failure! Domain %s added to DigitalOcean (%s) with ID: %s', newdata, domain_name)

return result


def getcurrentremoteip(domain):
current = config.items(section=domain)
subdomainid = current[0][1]
req = urllib.request.Request('https://api.digitalocean.com/v2/domains/' + domain_name + '/records/' +
subdomainid)
req.add_header('Content-Type', 'application/json')
req.add_header('Authorization', 'Bearer ' + api)
current = urllib.request.urlopen(req)
remote = current.read().decode('utf-8')
remotedata = json.loads(remote)
remoteip4 = remotedata['domain_record']['data']
return remoteip4


def remove_from_config(name):
try:
config.remove_section(name)
logging.info(time.strftime("%Y-%m-%d %H:%M")+'Success! Domain %s removed from config.ini', name)
with open(filepath+'config.ini', 'w') as f:
config.write(f)
result = True
except:
result = False
logging.error(time.strftime("%Y-%m-%d %H:%M")+'Failure! Domain %s not removed from config.ini', name)

return result



Loading…
Cancel
Save