Update a file through a Python script
Summary
In this project, I developed a command-line tool to manage an allow list of IP addresses, which is utilized to control access to restricted content within an organization. Written in Python, this tool is designed to offer a seamless way to add or remove IP addresses from the allow list, while also ensuring that the list remains up-to-date.
Key features
- File Handling: Upon being called from the command line
the tool checks whether the allow list file
allow_list.txtexists and reads its content if available. - Adding IP addresses: Upon typing a simple command flag (
--add) users can add one or more IP addresses to the allow list. - Removing IP addresses: Conversely, users can remove one or more
IP addresses using the command flag (
--remove), revoking access from spceific IPs as needed. - Ease of use: The tool is built to be used from the command line, making it accessible for administrators and engineers who may need to modify the allow list.
- Automated management: By automating the process of updating the allow list, this tool significantly reduces manual effort, eliminates human error, and ensures that the access controls are always current.
Technologies used
The script is written in Python and makes use of the following libraries:os: this library provides a way of interfacing with the underlying operating system. In the code, it is used to check whether the allow list file exists.argparse: this library is used to parse command-line arguments. It makes it easy to write user-friendly command-line interfaces, allowing the script to define what arguments it requires and how these arguments should be handled.re: this module provides regular expression matching operations, which the script uses to validate the IP passed as parameters according to IPv4 and IPv6 formats.
Steps
After importing the necessary libraries, the code defines
the function update_allow_list(), responsible for
the heavy lifting in the script by first creating the list –
if none exists – and then updating it by adding or removing
the IPs as commanded, after a simple input validation.
Step 1
Naturaly, the code starts by importing aforementioned libraries:
import os
import argparse
import re
Step 2
A helper function is_valid_ip() is defined and implemented,
in order to check whether the IPs passed in as parameters conform to IPv4 or IPv6
format. If so, it returs True; if not, it returns False, and prints an error
message to the console.
def is_valid_ip(ip_address):
# Pattern for IPv4
pattern_ipv4 = r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
# Pattern for IPv6
pattern_ipv6 = r'^((([0-9A-Fa-f]{1,4}:){1,6}:)|(([0-9A-Fa-f]{1,4}:){7}))([0-9A-Fa-f]{1,4})$'
if bool(re.match(pattern_ipv4, ip_address) or re.match(pattern_ipv6, ip_address)):
return True
else:
print("{} is not a valid IP address.".format(ip_address))
return False
Step 3
The update_allow_list() function begins by initializing an empty
list of IP addresses – which will be populated with the IP addresses already
in the allow list (in the built-in file path allow_list.txt),
and eventually have its contents appended or removed as directed by the user.
The os.path module as the code calls its function exists()
which returns True if the parameter refers to an existing path
or an open file descriptor, and returns False for broken symbolic links.
Upon checking the existence of the file, the code opens it in read mode,
assiging the returning object to the file variable. Inside the block,
the files contents are read and split into lines with ip_address = file.read().splitlines().
By preceding the open() function with the keyword with,
the code ensures that the file is properly closed once its suite finishes
execution, even if an exception is raised within the block. This helps prevent
file leaks, and makes the code cleaner and more robust.
def update_allow_list(add_list=[], remove_list=[]):
ip_addresses = []
file_path = "allow_list.txt"
# Check if the file exists, and if it does, read the contents
if os.path.exists(file_path):
with open(file_path, "r") as file:
ip_addresses = file.read().splitlines()
Step 4
The code then iterates through the remove_list argument – iteration
which will only be executed if a remove list is present. Each IP in the remove list,
upon having its format positively checked, is then removed from the ip_addresses
list variable.
# Remove IPs from the list
for ip in remove_list:
if is_valid_ip(ip):
if ip in ip_addresses:
ip_addresses.remove(ip)
print("IP {} successfully removed from {}".format(ip, file_path))
else:
print("IP {} not found in {}".format(ip, file_path))
Step 5
The code also iterates through the add_list argument, in the same mode as
it does the remove_list. Upon input validation, the IP is appended to
the ip_addresses variable, or a message is displayed if it had already
been previously added.
# Add IPs to the list
for ip in add_list:
if is_valid_ip(ip):
if ip not in ip_addresses:
ip_addresses.append(ip)
print("IP {} sucessfully added to {}".format(ip, file_path))
else:
print("IP {} previously added to {}".format(ip, file_path))
Step 6
The str.join() function is then called to concatenate all the IPs in the ip_addresses
list, separated by lines. The buil-in open() function is then called once more, in write mode,
and saves the updated allow list – concluding the execution of the update_allow_list() function.
# Write the updated list to the file (this
# will create the file if it doesn't exist)
updated_ip_addresses = "\n".join(ip_addresses)
with open(file_path, "w") as file:
file.write(updated_ip_addresses)
Step 7
Since the script is run directly, and not imported as a module, this behaviour is enforced
by if __name__ == "__main__". Inside the block:
- an argument
parserobject is created in order to hold the information necessary to parse the command line into Python data types - the
add_argument()method defines the two optional arguments, for add and remove, respectively - the
nargs='+'parameter allows for one or more values for each argument (which will only be checked if the add or remove tag is used) - the
default=[]sets the value to an empty list if no argument is provided. parser.parse_args()parses the arguments provided on the command line and returns them as a namespace object, containing the values for each argument as defined – meaning: it will have two attributes:args.addandargs.remove, each containing the list of IP addresses passed for the corresponding command-line option- after the parser attributes have been extracted,
update_allow_list(args.add, args.remove)is then called, initiating the updating procedure.
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Update the allow list of IP addresses.')
parser.add_argument('-a', '--add', nargs='+', default=[], help='List of IP addresses to add.')
parser.add_argument('-r', '--remove', nargs='+', default=[], help='List of IP addresses to remove.')
args = parser.parse_args()
update_allow_list(args.add, args.remove)Challenges & Solutions
Despite being a somewhat simple script – and maybe because of it – the major challenge was to try to keep the code simple, easily readabla and maintainable, and, at the same time have a minimal amount of validation and responsiveness to the user. The solution was to try and find a balance between simplicity and robustness, where the input is validated, on IP at a time and the code is responsive to the user if an IP to be removed is not in list or an IP to be added is already there.
Code samples & Demo
This is the final version of the script:
Lessons learned
This script project underscored, for me, the utility of the language's straightforward syntax and
accessible libraries. The availability of tools like os, for file existence checking, and
argparse, for command-line parsing, allowed for the implementation of a practical
and functional script.
Conclusion
Overall, we can see Python's efficiency for handling file operations and custom user inputs without unnecessary complexity. Its simplicity, coupled with essential standard libraries, streamlined the development of the script – showcasing Python's role as a versatile and approachable programming language.