make iniset work with files that do not yet exist

This allows the add and set commands to work when files do not yet
exist, which becomes important for extracting things like post config
files.
This commit is contained in:
Sean Dague 2017-01-16 12:07:10 -05:00
parent 7e7ffec87e
commit e7636a7714
3 changed files with 153 additions and 11 deletions

View File

@ -16,6 +16,7 @@
# python ConfigFile parser because that ends up rewriting the entire # python ConfigFile parser because that ends up rewriting the entire
# file and doesn't ensure comments remain. # file and doesn't ensure comments remain.
import os.path
import re import re
import shutil import shutil
import tempfile import tempfile
@ -31,7 +32,10 @@ class IniFile(object):
"""Returns True if section has a key that is name""" """Returns True if section has a key that is name"""
current_section = "" current_section = ""
with open(self.fname) as reader: if not os.path.exists(self.fname):
return False
with open(self.fname, "r+") as reader:
for line in reader.readlines(): for line in reader.readlines():
m = re.match("\[([^\[\]]+)\]", line) m = re.match("\[([^\[\]]+)\]", line)
if m: if m:
@ -41,7 +45,6 @@ class IniFile(object):
return True return True
return False return False
def add(self, section, name, value): def add(self, section, name, value):
"""add a key / value to an ini file in a section. """add a key / value to an ini file in a section.
@ -50,26 +53,38 @@ class IniFile(object):
will be added to the end of the file. will be added to the end of the file.
""" """
temp = tempfile.NamedTemporaryFile(mode='r') temp = tempfile.NamedTemporaryFile(mode='r')
shutil.copyfile(self.fname, temp.name) if os.path.exists(self.fname):
shutil.copyfile(self.fname, temp.name)
else:
with open(temp.name, "w+"):
pass
found = False found = False
with open(temp.name) as reader: with open(self.fname, "w+") as writer:
with open(self.fname, "w") as writer: with open(temp.name) as reader:
for line in reader.readlines(): for line in reader.readlines():
writer.write(line) writer.write(line)
m = re.match("\[([^\[\]]+)\]", line) m = re.match("\[([^\[\]]+)\]", line)
if m and m.group(1) == section: if m and m.group(1) == section:
found = True found = True
writer.write("%s = %s\n" % (name, value)) writer.write("%s = %s\n" % (name, value))
if not found: if not found:
writer.write("[%s]\n" % section) writer.write("[%s]\n" % section)
writer.write("%s = %s\n" % (name, value)) writer.write("%s = %s\n" % (name, value))
def _at_existing_key(self, section, name, func, match="%s\s*\="): def _at_existing_key(self, section, name, func, match="%s\s*\="):
"""Run a function at a found key.
NOTE(sdague): if the file isn't found, we end up
exploding. This seems like the right behavior in nearly all
circumstances.
"""
temp = tempfile.NamedTemporaryFile(mode='r') temp = tempfile.NamedTemporaryFile(mode='r')
shutil.copyfile(self.fname, temp.name) shutil.copyfile(self.fname, temp.name)
current_section = "" current_section = ""
with open(temp.name) as reader: with open(temp.name) as reader:
with open(self.fname, "w") as writer: with open(self.fname, "w+") as writer:
for line in reader.readlines(): for line in reader.readlines():
m = re.match("\[([^\[\]]+)\]", line) m = re.match("\[([^\[\]]+)\]", line)
if m: if m:
@ -83,7 +98,6 @@ class IniFile(object):
else: else:
writer.write(line) writer.write(line)
def remove(self, section, name): def remove(self, section, name):
"""remove a key / value from an ini file in a section.""" """remove a key / value from an ini file in a section."""
def _do_remove(writer, line): def _do_remove(writer, line):
@ -91,7 +105,6 @@ class IniFile(object):
self._at_existing_key(section, name, _do_remove) self._at_existing_key(section, name, _do_remove)
def comment(self, section, name): def comment(self, section, name):
def _do_comment(writer, line): def _do_comment(writer, line):
writer.write("# %s" % line) writer.write("# %s" % line)
@ -112,3 +125,42 @@ class IniFile(object):
self._at_existing_key(section, name, _do_set) self._at_existing_key(section, name, _do_set)
else: else:
self.add(section, name, value) self.add(section, name, value)
class LocalConf(object):
"""Class for manipulating local.conf files in place."""
def __init__(self, fname):
self.fname = fname
def _conf(self, group, conf):
in_section = False
current_section = ""
with open(self.fname) as reader:
for line in reader.readlines():
if re.match(r"\[\[%s\|%s\]\]" % (
re.escape(group),
re.escape(conf)),
line):
in_section = True
continue
# any other meta section means we aren't in the
# section we want to be.
elif re.match("\[\[.*\|.*\]\]", line):
in_section = False
continue
if in_section:
m = re.match("\[([^\[\]]+)\]", line)
if m:
current_section = m.group(1)
continue
else:
m2 = re.match(r"(\w+)\s*\=\s*(.+)", line)
if m2:
yield current_section, m2.group(1), m2.group(2)
def extract(self, group, conf, target):
ini_file = IniFile(target)
for section, name, value in self._conf(group, conf):
ini_file.set(section, name, value)

View File

@ -110,3 +110,22 @@ class TestIniSet(testtools.TestCase):
with open(self._path) as f: with open(self._path) as f:
content = f.read() content = f.read()
self.assertEqual(content, RESULT4) self.assertEqual(content, RESULT4)
class TestIniCreate(testtools.TestCase):
def setUp(self):
super(TestIniCreate, self).setUp()
self._path = self.useFixture(fixtures.TempDir()).path
self._path += "/test.ini"
def test_add_items(self):
conf = dsconf.IniFile(self._path)
conf.set("default", "c", "d")
conf.set("default", "a", "b")
conf.set("second", "g", "h")
conf.set("second", "e", "f")
conf.set("new", "s", "t")
with open(self._path) as f:
content = f.read()
self.assertEqual(BASIC, content)

View File

@ -0,0 +1,71 @@
# Copyright 2017 IBM
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# Implementation of ini add / remove for devstack. We don't use the
# python ConfigFile parser because that ends up rewriting the entire
# file and doesn't ensure comments remain.
import fixtures
import os.path
import testtools
from devstack import dsconf
BASIC = """
[[local|localrc]]
a = b
c = d
f = 1
[[post-config|$NEUTRON_CONF]]
[DEFAULT]
global_physnet_mtu=1450
[[post-config|$NOVA_CONF]]
[upgrade_levels]
compute = auto
"""
NOVA = """[upgrade_levels]
compute = auto
"""
NEUTRON = """[DEFAULT]
global_physnet_mtu = 1450
"""
class TestLcExtract(testtools.TestCase):
def setUp(self):
super(TestLcExtract, self).setUp()
self._path = self.useFixture(fixtures.TempDir()).path
self._path += "/local.conf"
with open(self._path, "w") as f:
f.write(BASIC)
def test_extract_neutron(self):
dirname = self.useFixture(fixtures.TempDir()).path
neutron = os.path.join(dirname, "neutron.conf")
nova = os.path.join(dirname, "nova.conf")
conf = dsconf.LocalConf(self._path)
conf.extract("post-config", "$NEUTRON_CONF", neutron)
conf.extract("post-config", "$NOVA_CONF", nova)
with open(neutron) as f:
content = f.read()
self.assertEqual(content, NEUTRON)
with open(nova) as f:
content = f.read()
self.assertEqual(content, NOVA)