views:

1056

answers:

5

I need to update a text file whenever my IP address changes, and then run a few commands from the shell afterwards.

  1. Create variable LASTKNOWN = "212.171.135.53" This is the ip address we have while writing this script.

  2. Get the current IP address. It will change on a daily basis.

  3. Create variable CURRENT for the new IP.

  4. Compare (as strings) CURRENT to LASTKNOWN

  5. If they are the same, exit()

  6. If they differ,

    A. "Copy" the old config file (/etc/ipf.conf) containing LASTKNOWN IP address into /tmp B. Replace LASTKNOWN with CURRENT in the /tmp/ipf.conf file.
    C. Using subprocess "mv /tmp/ipf.conf /etc/ipf.conf"
    D. Using subprocess execute, "ipf -Fa -f /etc/ipf.conf"
    E. Using subprocess execute, "ipnat -CF -f /etc/ipnat.conf"

  7. exit()

I know how to do steps 1 through 6. I fall down on the "file editing" part, A -> C. I can't tell what module to use or whether I should be editing the file in place. There are so many ways to do this, I can't decide on the best approach. I guess I want the most conservative one.

I know how to use subprocess, so you don't need to comment on that.

I don't want to replace entire lines; just a specific dotted quad.

Thanks!

+1  A: 

Probably the simplest way would be to open a file using f=open(filename, mode). Then, read all the lines using f.readlines() (this will return a list of strings representing the lines of the program).

You can then search these strings to find the address and replace it with the new one (using standard string replacing, regular expressions, or whatever you want).

At the end, you can write the lines back to the file using f.writelines(lines), which conveniently takes back a list of lines.

NOTE: This is not an efficient way to do this, it's just the easiest. Please

Example code:

f = open(filename, "r")
lines = f.readlines()

# Assume that change_ip is a function that takes a string and returns a new one with the ip changed): example below
ret_lines = [change_ip(lines) for line in lines]
new_file = open(new_filename, "w")
new_file.writelines(lines)

def change_ip(str):
   ''' Gets a string, returns a new string where the ip is changed '''
   # Add implementation, something like: return str.replace(old_ip, new_ip) or something similair.
Edan Maor
Thanks. I'll try this. It's one of the paths I was starting down.
If the file is one that is read and monitored by other processes, this approach may result in the other processes seeing an inconsistent state, perhaps with nonsensical contents. If that's a concern, write to a temp, sync that file, then move it into the right place.
swillden
Not sure what you mean by "sync" in this context. Sync to what from what?
Also, "change_ip" looks like a "verb" but I think you intend for me to put the string for the NEW IP address there. Remember, I already have the "known" and "current" IP addresses stored as variables of type str()
LASTKNOWN = '175.48.234.168'import socketimport fileinputimport subprocessimport stringimport reCURRENT = socket.getaddrinfo(socket.gethostname(), None)[0][4][0]# the above get my actual current IPif CURRENT == LASTKNOWN: print 'Nevermind.' subprocess.sys.exit()else:
OH. You're saying I need to WRITE a function for "chage_ip". Heh.
Yep, I'm assuming change_ip is a function which does whatever logic you need to search a string and change the ip in it (thereby returning a new string with the ip change). I'll edit to make it clearer.
Edan Maor
And doesn't the def change_ip(str): block need to go ABOVE the loop?Because it HAS to be a loop that checks each line of the file one by one.
# Still borken import socket import fileinput import subprocess import string CURRENT = socket.getaddrinfo(socket.gethostname(), None)[0][4][0] LASTKNOWN = '175.48.204.168' if CURRENT == LASTKNOWN: print 'Nevermind.' subprocess.sys.exit() else: def change_ip(string): return string.replace(LASTKNOWN, CURRENT) f = open("/tmp/iiiipf.conf", "r") lines = f.readline() for line in lines: ret_lines = [change_ip(lines) for line in lines] new_file = open("/tmp/ipf.conf", "w") new_file.writelines(lines)
http://pastebin.com/m1be5336c
The above pastebin link shows the script as it is now. All it can do is write a single line to the output file.
+1  A: 

You're trying to "atomically" update the contents of a file, and there have been many delightful flame wars on the subject. But the general pattern is:

1) Write the new file to a temp file, and make sure you flush and close.

2) Use your operating system's facilities to atomically rename the temp file to the old file.

Now, you simply can't atomically rename a file in Windows, but it sounds like you're on a unix-like system anyway. You atomically rename using os.rename().

Jonathan Feinberg
+1: "update in place" is a crazy thing to try. It's simpler to create new and rename.
S.Lott
Yes, this script will run on BSD.Yeah, I have never been able to figure out how to "edit files in place" using Python. It may be possible but it doesn't seem easy.
So... does my answer provide what you needed?
Jonathan Feinberg
Nope. "Write the new file to a temp file" using what? Does the new file already have the new IP address in it or not? Isn't it more like, "Copy the existing file into /tmp and perform the string substitution on it there then move it into /etc"?
There are three steps at least involved ;)
The problem is that it's not clear what your question is. I was answering a question about how to execute an atomic file update. Are you also asking how to read the contents of a file as text?
Jonathan Feinberg
When you "copy a file" you are doing several things. You are *creating* a new file; you are *opening* it; you are *writing* data into it; you are closing it. So rather than "copying the file and then changing it," (create, open, write, close, open, rewrite, close) just open the new file, and write the correct information into it the first time (create open, write, close). Writing the wrong information in and then fixing it is slow and excessively complex.
jcdyer
A: 

After checking the comments and the bit of code that you put on pastebin, here is a working solution. To begin with, the file /tmp/iiiipf.conf contains:

Simply a test file 175.48.204.168

And two times 175.48.204.168 on this line 175.48.204.168

Done.

After running the code, the file /tmp/iiiipf.conf contains:

Simply a test file 10.73.144.112

And two times 10.73.144.112 on this line 10.73.144.112

Done.

And here is the tested working code with my stuff merged into your pastebin code:

import socket
import fileinput
import subprocess
import string
import re

CURRENT = socket.getaddrinfo(socket.gethostname(), None)[0][4][0]
LASTKNOWN = '175.48.204.168'

if CURRENT == LASTKNOWN:
    print 'Nevermind.'
    subprocess.sys.exit()

else:

    cf = open("/tmp/iiiipf.conf", "r")
    lns = cf.readlines()
    # close it so that we can open for writing later
    cf.close()

    # assumes LASTKNOWN and CURRENT are strings with dotted notation IP addresses
    lns = "".join(lns)
    lns = re.sub(LASTKNOWN, CURRENT, lns)  # This replaces all occurences of LASTKNOWN with CURRENT

    cf = open("/tmp/iiiipf.conf", "w")
    cf.write(lns)
    cf.close()

This bit of code will do what you need even if the IP address is used several times within the configuration file. It will also change it in comment lines.

This method does not require copying to /tmp and uses one less subprocess call when restarting the firewall and NAT.

Michael Dillon
Thanks, this looks like it will work. However ipf and ipnat will still need to be "restarted." Ipfilter can't see changes to its config file by itself. It needs to have the old rules flushed and the new rules installed. It also makes sense to "refresh" ipnat as well. I have used ipf/ipnat since 1999 and this is the only reliable way to do it.
When I tried to use the above I got:root@spork# ./pyifchk.py Traceback (most recent call last): File "./pyifchk.py", line 22, in <module> lns = re.sub(LASTKNOWN,CURRENT,lns) File "/usr/local/lib/python2.5/re.py", line 150, in sub return _compile(pattern, 0).sub(repl, string, count)TypeError: expected string or buffer
That's why I said "code fragment". You would need to insert this into your script which does the other stuff, including assigning a string value to CURRENT and LASTKNOWN. Or if you have different variable names for the strings holding the IP address, then change the names in my example. I'm relying on you to know where and when to call subprocess to stop and start the firewall and NAT because I'm not that familiar with Solaris details.
Michael Dillon
Again, not asking for help with BSD or Solaris or ipfilter.lns = cf.readlines() returns a LIST, not a string.
Should be: lns = cf.readline()not cf.readlines
No, should be: `lns = (re.sub(LASTKNOWN, CURRENT, l) for l in lns)`. Calling `readline()` just gets the first line in the file; that generator expression does the substitution on every line and returns the result as an iterable, which `write` will iterate through.
Robert Rossney
What kind of structure is that? Reminds me of list comprehension.
D'oh. You said generator. Yeah.
File "./pyifchk.py", line 22 lns = (re.sub(LASTKNOWN, CURRENT, 1) for 1 in lns)SyntaxError: can't assign to literal
And if I change the 1 to an l (stupid fscking charset) I get a different error! Yay!!!root@spork# ./pyifchk.py Traceback (most recent call last): File "./pyifchk.py", line 24, in <module> cf.write(lns)TypeError: argument 1 must be string or read-only character buffer, not generator
My bad. readlines does return a list so you need to make it a string like so: lns = "".join(lns) and that will give you the whole config in a string. Just add that after the readlines() line in my example above.
Michael Dillon
readline. thanks!What about coersing it into a string with str() ?
Oh, there is readline and there is also readlines. How utterly inconvenient and confusing. Yay!
Yeah I'm not with ja. I thought ''' was one way to comment out code.
Oh, double quote. Sorry.
Still fails.http://pastebin.com/m391e399a Is this what you intended? BTW, you don't need BSD to test this. Just substitute some kind of ifconfig for my socket.getaddrinfo command.
I looked at your pastebin code which still doesn't work. In particular, compare the re.sub line above with what you had. This time I made up a dummy config file and tested the whole example so I know that it works. I edited the answer above to reflect the bugfixes etc.
Michael Dillon
OK. I have something that works for now. Thanks for the help.There is one major flaw in my logic though. Once my IP changes the LASTKNOWN variable becomes a liability since it's not the last known IP address anymore. I guess I need a different test.The pseudo code would look like this: "If CURRENT ip is not in /etc/ipf.conf, replace whatever IP address you find in /etc/ipf.conf UNLESS the IP is 255.255.255.255 or 0.0.0.0"
+1  A: 

Replace LASTKNOWN by CURRENT in /etc/ipf.conf

Replace all at once

filename = "/etc/ipf.conf"
text = open(filename).read()
open(filename, "w").write(text.replace(LASTKNOWN, CURRENT))

Replace line by line

from __future__ import with_statement
from contextlib import nested

in_filename, outfilename = "/etc/ipf.conf", "/tmp/ipf.conf"
with nested(open(in_filename), open(outfilename, "w")) as in_, out:
     for line in in_:
         out.write(line.replace(LASTKNOWN, CURRENT))
os.rename(outfilename, in_filename)

Note: "/tmp/ipf.conf" should be replaced by tempfile.NamedTemporaryFile() or similar
Note: the code is not tested.

J.F. Sebastian
Yeah. Thanks. ./pyifchk-0.py:23: Warning: 'with' will become a reserved keyword in Python 2.6 File "./pyifchk-0.py", line 23 with nested(open(in_filename), open(outfilename, "w")) as in_, out: ^SyntaxError: invalid syntax
The ^ points at where "nested" and "(in_filename)" are joined.I tried it with a space between them with nested (in_filename), open(outfilename, "w")) as in_, out: and got the same error.
@javadong: on Python 2.5 you need `from __future__ import with_statement`.
J.F. Sebastian
+1  A: 

Another way to simply edit files in place is to use the fileinput module:

import fileinput, sys
for line in fileinput.input(["test.txt"],inplace=True):
    line=line.replace("car","truck")
    #sys.stdout is redirected to the file
    sys.stdout.write(line)
Tim Jones