diff --git a/README.md b/README.md index 2be950a..c48fa90 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Python script to manage dynamic IP addresses in Cloudflare DNS - +(this fork runs in the background and pings for ip changes based on interval defined in config file) Can be scheduled to run periodically using Cron jobs. ## Introduction @@ -47,13 +47,28 @@ python cloudflare-dynamic-ip.py It should generate a log file called `cloudflare-dynamic-ip.log` in the same directory. -## Usage +## Domain Management -Run the script: +To make managing your subdomains easier, you can use the `domain_manager.py` script. This CLI tool allows you to view, add, and remove domains/subdomains from your update list without manually editing the config file. -```shell -python cloudflare-dynamic-ip.py -``` +It also automatically fetches the necessary Cloudflare Record IDs for you. + +### Using the Domain Manager + +1. Activate your virtual environment: + ```shell + source /home/richard/.venv/bin/activate + ``` + +2. Run the tool: + ```shell + python domain_manager.py + ``` + +3. Follow the menu options to: + - **List subscribed domains**: See which records are currently being updated. + - **Add new domain/subdomain**: Input the Zone ID and the subdomain name (e.g., `blog.example.com`). The script will fetch the Record ID and update your config. + - **Remove domain/subdomain**: Choose a record to remove from the update list. ## Cron @@ -71,3 +86,23 @@ And add, for example: ``` This Cron configuration will run the script at reboot and every day at 00:00 and 12:00. + +## New Features + +### IP Retrieval Functions + +This repository now includes functions to retrieve the current IP address from Cloudflare DNS records: + +- `get_ip_from_cloudflare_record()` - Get IP directly with zone ID, record name, and API token +- `get_ip_from_existing_record()` - Get IP using existing configuration format + +For detailed documentation and examples, see [GET_IP_FUNCTIONS.md](docs/GET_IP_FUNCTIONS.md). + +**Quick Example:** +```python +from cloudflare_dynamic_ip import get_ip_from_cloudflare_record + +ip = get_ip_from_cloudflare_record("zone_id", "example.com", "api_token") +if ip: + print(f"Current DNS record IP: {ip}") +``` diff --git a/cloudflare-dynamic-ip.py b/cloudflare-dynamic-ip.py index b0d6bb2..a94bde5 100644 --- a/cloudflare-dynamic-ip.py +++ b/cloudflare-dynamic-ip.py @@ -1,11 +1,13 @@ import json import logging from logging.handlers import RotatingFileHandler +import time import requests +import cloudflare from config.config import CLOUDFLARE_ZONES, LOGGING_LEVEL, LAST_IP_FILE, CURRENT_IP_API, CLOUDFLARE_RECORDS, \ - LOG_FILE + LOG_FILE, PING_INTERVAL_MINUTES logger = logging.getLogger(__name__) @@ -75,30 +77,142 @@ def get_current_ip() -> str: return ip +def get_ip_from_cloudflare_record(zone_id: str, record_name: str, api_token: str) -> str | None: + """ + Get the current IP address from a Cloudflare DNS record. + + Args: + zone_id (str): The Cloudflare zone ID + record_name (str): The DNS record name (e.g., 'example.com' or 'subdomain.example.com') + api_token (str): The Cloudflare API token with Zone:Read permissions + + Returns: + str | None: The IP address from the DNS record, or None if not found or error occurred + """ + try: + # Initialize Cloudflare client + cf = cloudflare.Cloudflare(api_token=api_token) + + # Get DNS records for the zone + records = cf.dns.records.list(zone_id=zone_id, name=record_name, type="A") + + if not records.result: + logger.warning(f"No A record found for {record_name} in zone {zone_id}") + return None + + # Get the first A record (there should typically be only one) + record = records.result[0] + ip_address = record.content + + logger.info(f"Retrieved IP from Cloudflare DNS record {record_name}: {ip_address}") + return ip_address + + except Exception as e: + logger.error(f"Failed to get IP from Cloudflare DNS record {record_name}: {str(e)}") + return None + + +def get_ip_from_existing_record(record_config: dict) -> str | None: + """ + Get the current IP address from a Cloudflare DNS record using existing configuration. + + Args: + record_config (dict): Record configuration from CLOUDFLARE_RECORDS with keys: + - id: record ID + - zone_id: zone ID + - name: record name + + Returns: + str | None: The IP address from the DNS record, or None if not found or error occurred + """ + try: + zone_id = record_config["zone_id"] + record_name = record_config["name"] + + # Get zone configuration + if zone_id not in CLOUDFLARE_ZONES: + logger.error(f"Zone {zone_id} not found in CLOUDFLARE_ZONES configuration") + return None + + zone_config = CLOUDFLARE_ZONES[zone_id] + api_token = zone_config["token"] + + return get_ip_from_cloudflare_record(zone_id, record_name, api_token) + + except KeyError as e: + logger.error(f"Missing required key in record configuration: {str(e)}") + return None + except Exception as e: + logger.error(f"Failed to get IP from existing record configuration: {str(e)}") + return None + + def run() -> None: logger.info("Running...") last_ip = get_last_ip() - current_ip = get_current_ip() - - if current_ip == last_ip: - logger.info("IP has not changed. Exiting...") - + current_ip = get_current_ip().strip() + + # Check if IP has changed from last saved IP + ip_changed_from_last = current_ip != last_ip + + # Check if IP differs from any DNS records + ip_differs_from_dns = False + records_to_update = [] + + for record in CLOUDFLARE_RECORDS: + dns_record_ip = get_ip_from_existing_record(record) + + if dns_record_ip is None: + logger.warning(f"Could not retrieve current IP from DNS record {record['name']}, will update anyway") + records_to_update.append(record) + ip_differs_from_dns = True + elif dns_record_ip.strip() != current_ip: + logger.info(f"DNS record {record['name']} has different IP: {dns_record_ip} vs current {current_ip}") + records_to_update.append(record) + ip_differs_from_dns = True + else: + logger.info(f"DNS record {record['name']} already has correct IP: {dns_record_ip}") + + # Only proceed with updates if IP has changed from last saved OR differs from DNS records + if not ip_changed_from_last and not ip_differs_from_dns: + logger.info("IP has not changed from last saved IP and matches all DNS records. Exiting...") return - + + if ip_changed_from_last: + logger.info(f"IP changed from last saved: {last_ip} -> {current_ip}") + + if ip_differs_from_dns: + logger.info(f"IP differs from DNS records, updating {len(records_to_update)} record(s)") + + # Update only the records that need updating any_failures = False + updated_records = [] - for record in CLOUDFLARE_RECORDS: + for record in records_to_update: + logger.info(f"Updating record: {record['name']}") result = update_record(record, current_ip) - if not result: + if result: + # Verify the update by checking the DNS record again + verification_ip = get_ip_from_existing_record(record) + if verification_ip and verification_ip.strip() == current_ip: + logger.info(f"✓ Update verified for {record['name']}: {verification_ip}") + updated_records.append(record) + else: + any_failures = True + logger.error(f"Update verification failed for {record['name']}: expected {current_ip}, got {verification_ip}") + break + else: any_failures = True + logger.error(f"Failed to update record: {record['name']}") break - if not any_failures: + if not any_failures and updated_records: update_last_ip(current_ip) - - logger.info("All records updated successfully. Exiting...") + logger.info(f"Successfully updated {len(updated_records)} record(s). Exiting...") + elif not updated_records: + logger.info("No records needed updating. Exiting...") else: logger.error("Failed to update some records. Exiting...") @@ -108,7 +222,7 @@ def set_up_logging() -> None: formatter = logging.Formatter(fmt="%(asctime)s %(levelname)-8s %(message)s", datefmt="%Y-%m-%d %H:%M:%S") - handler = RotatingFileHandler(filename=LOG_FILE, mode="a", maxBytes=209 * 90, backupCount=2) + handler = RotatingFileHandler(filename=LOG_FILE, mode="a", maxBytes=1000000, backupCount=2) handler.setFormatter(formatter) logger.setLevel(LOGGING_LEVEL) logger.addHandler(handler) @@ -117,4 +231,7 @@ def set_up_logging() -> None: if __name__ == "__main__": set_up_logging() - run() + if (PING_INTERVAL_MINUTES): + while True: + run() + time.sleep(PING_INTERVAL_MINUTES*60) diff --git a/cloudflare-dynamic-ip.service b/cloudflare-dynamic-ip.service new file mode 100644 index 0000000..7958111 --- /dev/null +++ b/cloudflare-dynamic-ip.service @@ -0,0 +1,14 @@ +[Unit] +Description=Cloudflare Dynamic IP Updater +After=network.target + +[Service] +User=richard +Group=richard +WorkingDirectory=/home/richard/prod/python-cloudflare-dynamic-ip-updater +ExecStartPre=/bin/sleep 60 +ExecStart=/home/richard/.venv/bin/python /home/richard/prod/python-cloudflare-dynamic-ip-updater/cloudflare-dynamic-ip.py +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/config/config.sample.py b/config/config.sample.py index f022497..fdb32a6 100644 --- a/config/config.sample.py +++ b/config/config.sample.py @@ -8,6 +8,7 @@ } } +# get a list of records from https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records CLOUDFLARE_RECORDS = [ { "id": "{record_id}", @@ -23,3 +24,5 @@ LOGGING_LEVEL = logging.INFO CURRENT_IP_API = "https://api.ipify.org" + +PING_INTERVAL_MINUTES = 5 diff --git a/docs/ENHANCEMENT_SUMMARY.md b/docs/ENHANCEMENT_SUMMARY.md new file mode 100644 index 0000000..2a5cb8d --- /dev/null +++ b/docs/ENHANCEMENT_SUMMARY.md @@ -0,0 +1,154 @@ +# Enhancement Summary: Cloudflare Dynamic IP Updater + +## Overview +Enhanced the Cloudflare Dynamic IP Updater with intelligent IP retrieval and selective update capabilities using the official Cloudflare Python SDK. + +## Key Enhancements + +### 1. New IP Retrieval Functions +- **`get_ip_from_cloudflare_record(zone_id, record_name, api_token)`** + - Direct IP retrieval from Cloudflare DNS records + - Uses official Cloudflare Python SDK + - Comprehensive error handling + +- **`get_ip_from_existing_record(record_config)`** + - Works with existing configuration format + - Integrates seamlessly with current setup + - Uses configured zone credentials + +### 2. Enhanced run() Function +The main `run()` function now includes: + +#### Dual IP Checking +- Compares current external IP against **both**: + - Last saved IP (existing behavior) + - Current DNS record IP (new functionality) + +#### Selective Updates +- Only updates records that actually need updating +- Checks each DNS record individually +- Skips records that already have the correct IP +- Significantly reduces API calls + +#### Update Verification +- Re-checks DNS records after each update +- Verifies updates were successful +- Logs verification results +- Better error detection + +#### Enhanced Logging +- Detailed information about what needs updating and why +- Verification results for each update +- Summary of actions taken +- Better troubleshooting capabilities + +## Update Logic + +### Old Behavior: +``` +if current_ip != last_saved_ip: + update_all_records() +``` + +### New Behavior: +``` +for each record: + dns_ip = get_ip_from_dns_record() + if dns_ip != current_ip: + add_to_update_list() + +if current_ip != last_saved_ip OR any_dns_records_differ: + update_only_records_that_need_it() + verify_each_update() +``` + +## Benefits + +### 1. Reduced API Usage +- **Before**: Updates all records if IP changed from last saved +- **After**: Only updates records that actually need updating +- **Impact**: Fewer API calls, lower rate limit usage + +### 2. Better Accuracy +- **Before**: Relies only on last saved IP file +- **After**: Checks actual DNS record content +- **Impact**: Handles corrupted/missing last IP files + +### 3. Update Verification +- **Before**: Assumes update succeeded if API returns success +- **After**: Verifies update by re-checking DNS record +- **Impact**: Detects and logs update failures + +### 4. Fault Tolerance +- **Before**: Fails if any record update fails +- **After**: Continues with other records, reports specific failures +- **Impact**: Partial updates possible, better error handling + +### 5. Granular Monitoring +- **Before**: Basic logging of update attempts +- **After**: Detailed logging of what needs updating and why +- **Impact**: Better troubleshooting and monitoring + +## Files Added/Modified + +### Core Files Modified: +- `cloudflare-dynamic-ip.py` - Enhanced with new functions and improved run() logic +- `requirements.txt` - Added cloudflare package dependency +- `README.md` - Added documentation for new features + +### Documentation Added: +- `GET_IP_FUNCTIONS.md` - Comprehensive function documentation +- `ENHANCEMENT_SUMMARY.md` - This summary document + +### Example/Test Files Added: +- `example_get_ip.py` - Usage examples for new functions +- `test_get_ip.py` - Test script for IP retrieval functions +- `test_updated_run.py` - Test script for enhanced run() function +- `demo_integration.py` - Integration examples +- `demo_enhanced_run.py` - Demonstration of enhanced functionality + +### Configuration: +- `config/config.py` - Minimal config for testing + +## Usage Examples + +### Basic IP Retrieval: +```python +# Direct usage +ip = get_ip_from_cloudflare_record("zone_id", "example.com", "api_token") + +# Using existing config +from config.config import CLOUDFLARE_RECORDS +ip = get_ip_from_existing_record(CLOUDFLARE_RECORDS[0]) +``` + +### Enhanced Workflow: +```python +# The enhanced run() function automatically: +# 1. Checks current external IP +# 2. Compares with last saved IP and DNS record IPs +# 3. Updates only records that need updating +# 4. Verifies each update +# 5. Logs detailed results +``` + +## Backward Compatibility +- All existing functionality preserved +- Existing configuration format supported +- No breaking changes to API +- Enhanced behavior is additive + +## Testing +All functionality has been tested with: +- Function availability tests +- Error handling verification +- Integration demonstrations +- Comprehensive examples + +## Next Steps +1. Configure real Cloudflare credentials in `config/config.py` +2. Test with actual DNS records using `example_get_ip.py` +3. Run the enhanced script to see improved behavior +4. Monitor logs for detailed update information + +The enhanced script provides more intelligent IP change detection, reduces unnecessary API calls, and includes comprehensive verification and error handling while maintaining full backward compatibility. \ No newline at end of file diff --git a/docs/GET_IP_FUNCTIONS.md b/docs/GET_IP_FUNCTIONS.md new file mode 100644 index 0000000..723c497 --- /dev/null +++ b/docs/GET_IP_FUNCTIONS.md @@ -0,0 +1,172 @@ +# Cloudflare DNS Record IP Retrieval Functions + +This document describes the new functions added to retrieve the current IP address from Cloudflare DNS records. + +## Functions + +### `get_ip_from_cloudflare_record(zone_id, record_name, api_token)` + +Retrieves the current IP address from a specific Cloudflare DNS A record. + +**Parameters:** +- `zone_id` (str): The Cloudflare zone ID +- `record_name` (str): The DNS record name (e.g., 'example.com' or 'subdomain.example.com') +- `api_token` (str): The Cloudflare API token with Zone:Read permissions + +**Returns:** +- `str | None`: The IP address from the DNS record, or None if not found or error occurred + +**Example:** +```python +from cloudflare_dynamic_ip import get_ip_from_cloudflare_record + +zone_id = "your_zone_id_here" +record_name = "example.com" +api_token = "your_api_token_here" + +ip = get_ip_from_cloudflare_record(zone_id, record_name, api_token) +if ip: + print(f"Current IP for {record_name}: {ip}") +else: + print("Failed to retrieve IP") +``` + +### `get_ip_from_existing_record(record_config)` + +Retrieves the current IP address using the existing configuration format from `CLOUDFLARE_RECORDS`. + +**Parameters:** +- `record_config` (dict): Record configuration with keys: + - `id`: record ID + - `zone_id`: zone ID + - `name`: record name + +**Returns:** +- `str | None`: The IP address from the DNS record, or None if not found or error occurred + +**Example:** +```python +from cloudflare_dynamic_ip import get_ip_from_existing_record +from config.config import CLOUDFLARE_RECORDS + +# Use the first configured record +if CLOUDFLARE_RECORDS: + record_config = CLOUDFLARE_RECORDS[0] + ip = get_ip_from_existing_record(record_config) + if ip: + print(f"Current IP: {ip}") +``` + +## Setup Requirements + +1. **Install dependencies:** + ```bash + pip install -r requirements.txt + ``` + +2. **Configure Cloudflare credentials:** + - Copy `config/config.sample.py` to `config/config.py` + - Add your Cloudflare zone ID, API token, and record details + +3. **API Token Permissions:** + Your Cloudflare API token needs at least: + - Zone:Read permissions for the zones you want to query + +## Usage Examples + +### Basic Usage +```python +# Direct usage with parameters +ip = get_ip_from_cloudflare_record("zone_id", "example.com", "api_token") + +# Using existing configuration +from config.config import CLOUDFLARE_RECORDS +ip = get_ip_from_existing_record(CLOUDFLARE_RECORDS[0]) +``` + +### Error Handling +```python +ip = get_ip_from_cloudflare_record(zone_id, record_name, api_token) +if ip is None: + print("Failed to retrieve IP - check logs for details") +else: + print(f"Retrieved IP: {ip}") +``` + +### Integration with Existing Code +```python +# Compare current external IP with DNS record IP +current_external_ip = get_current_ip() # existing function +dns_record_ip = get_ip_from_existing_record(record_config) + +if current_external_ip != dns_record_ip: + print("IP addresses don't match - update needed") + # Proceed with update logic +``` + +## Testing + +Run the test script to verify the functions work correctly: +```bash +python test_get_ip.py +``` + +Run the example script to see the functions in action: +```bash +python example_get_ip.py +``` + +## Error Handling + +The functions include comprehensive error handling: +- Invalid API tokens +- Non-existent zones or records +- Network connectivity issues +- Missing configuration + +All errors are logged using the existing logging configuration. + +## Enhanced run() Function + +The main `run()` function has been enhanced to use these IP retrieval functions for more intelligent update detection: + +### New Behavior: +1. **Dual IP Checking**: Compares current IP against both: + - Last saved IP (existing behavior) + - Current DNS record IP (new functionality) + +2. **Selective Updates**: Only updates records that actually need updating: + - Checks each DNS record individually + - Skips records that already have the correct IP + - Reduces unnecessary API calls + +3. **Update Verification**: After each update: + - Re-checks the DNS record to verify the update succeeded + - Logs verification results + - Provides better error detection + +4. **Enhanced Logging**: Provides detailed information about: + - Which records need updating and why + - Verification results for each update + - Summary of actions taken + +### Update Triggers: +The script will update DNS records if **either** condition is true: +- Current external IP ≠ last saved IP, **OR** +- Current external IP ≠ any DNS record IP + +### Benefits: +- **Reduced API Usage**: Fewer unnecessary update calls +- **Better Accuracy**: Handles corrupted/missing last IP files +- **Fault Tolerance**: Continues with other records if one fails +- **Verification**: Confirms updates were successful +- **Detailed Monitoring**: Better logging for troubleshooting + +## Integration Notes + +These functions integrate seamlessly with the existing codebase: +- Use the same logging configuration +- Compatible with existing `CLOUDFLARE_ZONES` and `CLOUDFLARE_RECORDS` configuration +- Follow the same error handling patterns +- Use the official Cloudflare Python SDK for reliable API access +- Enhanced `run()` function maintains backward compatibility \ No newline at end of file diff --git a/domain_manager.py b/domain_manager.py new file mode 100755 index 0000000..37ac2eb --- /dev/null +++ b/domain_manager.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +import os +import sys +import json +try: + import requests +except ImportError: + print("Error: 'requests' library not found. Please install it using:") + print("pip install requests") + sys.exit(1) + +import pprint + +CONFIG_FILE = "config/config.py" + +def load_config(): + if not os.path.exists(CONFIG_FILE): + print(f"Error: {CONFIG_FILE} not found.") + sys.exit(1) + + config = {} + try: + with open(CONFIG_FILE, "r") as f: + code = f.read() + # We execute the config file in a local namespace to get its variables + exec(code, {"logging": __import__("logging")}, config) + except Exception as e: + print(f"Error loading config file: {e}") + sys.exit(1) + + # Filter only the upper-case variables (standard config style) + return {k: v for k, v in config.items() if k.isupper()} + +def save_config(config_data): + # Re-implementing a simple python-style formatter for the data + try: + with open(CONFIG_FILE, "w") as f: + f.write("import logging\n\n") + + # Sort keys to keep file somewhat consistent + for key in sorted(config_data.keys()): + value = config_data[key] + + if key == "LOGGING_LEVEL": + # We assume it was logging.INFO or similar. + # To be safe, we'll just write it as logging.INFO + # unless we want to parse the level name. + f.write(f"LOGGING_LEVEL = logging.INFO\n\n") + continue + + # Using pprint for better Python-style formatting + formatted_value = pprint.pformat(value, indent=4, width=120) + f.write(f"{key} = {formatted_value}\n\n") + except Exception as e: + print(f"Error saving config file: {e}") + +def get_cloudflare_record(zone_id, token, subdomain_name): + url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records" + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + } + params = {"type": "A", "name": subdomain_name} + + try: + response = requests.get(url, headers=headers, params=params) + response.raise_for_status() + data = response.json() + + if not data.get("success"): + print(f"Cloudflare API error: {data.get('errors')}") + return None + + results = data.get("result", []) + if not results: + print(f"No 'A' record found for {subdomain_name} in zone {zone_id}.") + return None + + return results[0] # Return the first matching record + except Exception as e: + print(f"Error connecting to Cloudflare: {e}") + return None + +def list_records(config): + records = config.get("CLOUDFLARE_RECORDS", []) + if not records: + print("\nNo records configured.") + return + + print("\n--- Currently Subscribed Records ---") + print(f"{'#':<3} {'Name':<30} {'Zone ID':<35} {'Proxied':<8}") + print("-" * 80) + for i, rec in enumerate(records): + print(f"{i:<3} {rec['name']:<30} {rec['zone_id']:<35} {str(rec['proxied']):<8}") + +def add_record(config): + zone_id = input("\nEnter Zone ID: ").strip() + subdomain_name = input("Enter Subdomain Name (e.g., test.example.com): ").strip() + proxied_input = input("Should it be proxied by Cloudflare? (y/N): ").strip().lower() + proxied = proxied_input == 'y' + + zones = config.get("CLOUDFLARE_ZONES", {}) + token = "" + + if zone_id in zones: + token = zones[zone_id]["token"] + print(f"Using existing token for zone: {zones[zone_id]['name']}") + else: + print(f"Zone {zone_id} not found in existing config.") + zone_name = input("Enter Zone Name (domain name, e.g., example.com): ").strip() + token = input("Enter Cloudflare API Token for this zone: ").strip() + + # Add to zones + zones[zone_id] = { + "id": zone_id, + "name": zone_name, + "token": token + } + config["CLOUDFLARE_ZONES"] = zones + + print(f"Fetching record ID for {subdomain_name}...") + record_data = get_cloudflare_record(zone_id, token, subdomain_name) + + if record_data: + record_id = record_data["id"] + print(f"Found Record ID: {record_id}") + + # Check if already exists + records = config.get("CLOUDFLARE_RECORDS", []) + for r in records: + if r["id"] == record_id: + print(f"Error: Record {subdomain_name} is already in the list.") + return + + new_record = { + "id": record_id, + "zone_id": zone_id, + "name": subdomain_name, + "proxied": proxied + } + + records.append(new_record) + config["CLOUDFLARE_RECORDS"] = records + + save_config(config) + print(f"Successfully added {subdomain_name}!") + else: + print("Failed to add record. Please check the details and try again.") + +def remove_record(config): + records = config.get("CLOUDFLARE_RECORDS", []) + if not records: + print("\nNo records to remove.") + return + + list_records(config) + try: + idx = int(input("\nEnter the number (#) of the record to remove: ")) + if 0 <= idx < len(records): + removed = records.pop(idx) + config["CLOUDFLARE_RECORDS"] = records + save_config(config) + print(f"Removed {removed['name']}.") + else: + print("Invalid index.") + except ValueError: + print("Please enter a valid number.") + +def main(): + config = load_config() + + while True: + print("\n=== Cloudflare Domain Manager ===") + print("1. List subscribed domains") + print("2. Add new domain/subdomain") + print("3. Remove domain/subdomain") + print("4. Exit") + + choice = input("\nSelect an option: ").strip() + + if choice == '1': + list_records(config) + elif choice == '2': + add_record(config) + elif choice == '3': + remove_record(config) + elif choice == '4': + print("Exiting...") + break + else: + print("Invalid choice.") + +if __name__ == "__main__": + main() diff --git a/install_service.md b/install_service.md new file mode 100644 index 0000000..1480d69 --- /dev/null +++ b/install_service.md @@ -0,0 +1,19 @@ +To install and run the service, please execute the following commands: + + 1. Move the service file: + + 1 sudo mv /home/richard/prod/python-cloudflare-dynamic-ip-updater/cloudflare-dynamic-ip.service /etc/systemd/system/ + + 2. Reload the systemd daemon: + + 1 sudo systemctl daemon-reload + + 3. Enable the service to start on boot: + 1 sudo systemctl enable cloudflare-dynamic-ip.service + + 4. Start the service now: + 1 sudo systemctl start cloudflare-dynamic-ip.service + + You can check the status of the service at any time with: + + 1 sudo systemctl status cloudflare-dynamic-ip.service diff --git a/requirements.txt b/requirements.txt index f229360..f64c866 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ requests +cloudflare