summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean Dague <sean@dague.net>2017-01-16 12:07:10 -0500
committerSean Dague <sean@dague.net>2017-01-16 12:07:10 -0500
commite7636a77143b5f03611011e9dd2e367376d80b55 (patch)
tree8fad337da183b2a23a0773c768a0d659111a9c76
parent7e7ffec87ec2ed069bbb4d4a9743952d47470bc2 (diff)
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.
-rw-r--r--devstack/dsconf.py74
-rw-r--r--devstack/tests/test_ini_set.py19
-rw-r--r--devstack/tests/test_localconf_extract.py71
3 files changed, 153 insertions, 11 deletions
diff --git a/devstack/dsconf.py b/devstack/dsconf.py
index dcfd033..8173420 100644
--- a/devstack/dsconf.py
+++ b/devstack/dsconf.py
@@ -16,6 +16,7 @@
16# python ConfigFile parser because that ends up rewriting the entire 16# python ConfigFile parser because that ends up rewriting the entire
17# file and doesn't ensure comments remain. 17# file and doesn't ensure comments remain.
18 18
19import os.path
19import re 20import re
20import shutil 21import shutil
21import tempfile 22import tempfile
@@ -31,7 +32,10 @@ class IniFile(object):
31 """Returns True if section has a key that is name""" 32 """Returns True if section has a key that is name"""
32 33
33 current_section = "" 34 current_section = ""
34 with open(self.fname) as reader: 35 if not os.path.exists(self.fname):
36 return False
37
38 with open(self.fname, "r+") as reader:
35 for line in reader.readlines(): 39 for line in reader.readlines():
36 m = re.match("\[([^\[\]]+)\]", line) 40 m = re.match("\[([^\[\]]+)\]", line)
37 if m: 41 if m:
@@ -41,7 +45,6 @@ class IniFile(object):
41 return True 45 return True
42 return False 46 return False
43 47
44
45 def add(self, section, name, value): 48 def add(self, section, name, value):
46 """add a key / value to an ini file in a section. 49 """add a key / value to an ini file in a section.
47 50
@@ -50,26 +53,38 @@ class IniFile(object):
50 will be added to the end of the file. 53 will be added to the end of the file.
51 """ 54 """
52 temp = tempfile.NamedTemporaryFile(mode='r') 55 temp = tempfile.NamedTemporaryFile(mode='r')
53 shutil.copyfile(self.fname, temp.name) 56 if os.path.exists(self.fname):
57 shutil.copyfile(self.fname, temp.name)
58 else:
59 with open(temp.name, "w+"):
60 pass
61
54 found = False 62 found = False
55 with open(temp.name) as reader: 63 with open(self.fname, "w+") as writer:
56 with open(self.fname, "w") as writer: 64 with open(temp.name) as reader:
57 for line in reader.readlines(): 65 for line in reader.readlines():
58 writer.write(line) 66 writer.write(line)
59 m = re.match("\[([^\[\]]+)\]", line) 67 m = re.match("\[([^\[\]]+)\]", line)
60 if m and m.group(1) == section: 68 if m and m.group(1) == section:
61 found = True 69 found = True
62 writer.write("%s = %s\n" % (name, value)) 70 writer.write("%s = %s\n" % (name, value))
63 if not found: 71 if not found:
64 writer.write("[%s]\n" % section) 72 writer.write("[%s]\n" % section)
65 writer.write("%s = %s\n" % (name, value)) 73 writer.write("%s = %s\n" % (name, value))
66 74
67 def _at_existing_key(self, section, name, func, match="%s\s*\="): 75 def _at_existing_key(self, section, name, func, match="%s\s*\="):
76 """Run a function at a found key.
77
78 NOTE(sdague): if the file isn't found, we end up
79 exploding. This seems like the right behavior in nearly all
80 circumstances.
81
82 """
68 temp = tempfile.NamedTemporaryFile(mode='r') 83 temp = tempfile.NamedTemporaryFile(mode='r')
69 shutil.copyfile(self.fname, temp.name) 84 shutil.copyfile(self.fname, temp.name)
70 current_section = "" 85 current_section = ""
71 with open(temp.name) as reader: 86 with open(temp.name) as reader:
72 with open(self.fname, "w") as writer: 87 with open(self.fname, "w+") as writer:
73 for line in reader.readlines(): 88 for line in reader.readlines():
74 m = re.match("\[([^\[\]]+)\]", line) 89 m = re.match("\[([^\[\]]+)\]", line)
75 if m: 90 if m:
@@ -83,7 +98,6 @@ class IniFile(object):
83 else: 98 else:
84 writer.write(line) 99 writer.write(line)
85 100
86
87 def remove(self, section, name): 101 def remove(self, section, name):
88 """remove a key / value from an ini file in a section.""" 102 """remove a key / value from an ini file in a section."""
89 def _do_remove(writer, line): 103 def _do_remove(writer, line):
@@ -91,7 +105,6 @@ class IniFile(object):
91 105
92 self._at_existing_key(section, name, _do_remove) 106 self._at_existing_key(section, name, _do_remove)
93 107
94
95 def comment(self, section, name): 108 def comment(self, section, name):
96 def _do_comment(writer, line): 109 def _do_comment(writer, line):
97 writer.write("# %s" % line) 110 writer.write("# %s" % line)
@@ -112,3 +125,42 @@ class IniFile(object):
112 self._at_existing_key(section, name, _do_set) 125 self._at_existing_key(section, name, _do_set)
113 else: 126 else:
114 self.add(section, name, value) 127 self.add(section, name, value)
128
129
130class LocalConf(object):
131 """Class for manipulating local.conf files in place."""
132
133 def __init__(self, fname):
134 self.fname = fname
135
136 def _conf(self, group, conf):
137 in_section = False
138 current_section = ""
139 with open(self.fname) as reader:
140 for line in reader.readlines():
141 if re.match(r"\[\[%s\|%s\]\]" % (
142 re.escape(group),
143 re.escape(conf)),
144 line):
145 in_section = True
146 continue
147 # any other meta section means we aren't in the
148 # section we want to be.
149 elif re.match("\[\[.*\|.*\]\]", line):
150 in_section = False
151 continue
152
153 if in_section:
154 m = re.match("\[([^\[\]]+)\]", line)
155 if m:
156 current_section = m.group(1)
157 continue
158 else:
159 m2 = re.match(r"(\w+)\s*\=\s*(.+)", line)
160 if m2:
161 yield current_section, m2.group(1), m2.group(2)
162
163 def extract(self, group, conf, target):
164 ini_file = IniFile(target)
165 for section, name, value in self._conf(group, conf):
166 ini_file.set(section, name, value)
diff --git a/devstack/tests/test_ini_set.py b/devstack/tests/test_ini_set.py
index 383bfc7..19a7d6b 100644
--- a/devstack/tests/test_ini_set.py
+++ b/devstack/tests/test_ini_set.py
@@ -110,3 +110,22 @@ class TestIniSet(testtools.TestCase):
110 with open(self._path) as f: 110 with open(self._path) as f:
111 content = f.read() 111 content = f.read()
112 self.assertEqual(content, RESULT4) 112 self.assertEqual(content, RESULT4)
113
114
115class TestIniCreate(testtools.TestCase):
116
117 def setUp(self):
118 super(TestIniCreate, self).setUp()
119 self._path = self.useFixture(fixtures.TempDir()).path
120 self._path += "/test.ini"
121
122 def test_add_items(self):
123 conf = dsconf.IniFile(self._path)
124 conf.set("default", "c", "d")
125 conf.set("default", "a", "b")
126 conf.set("second", "g", "h")
127 conf.set("second", "e", "f")
128 conf.set("new", "s", "t")
129 with open(self._path) as f:
130 content = f.read()
131 self.assertEqual(BASIC, content)
diff --git a/devstack/tests/test_localconf_extract.py b/devstack/tests/test_localconf_extract.py
new file mode 100644
index 0000000..2a5f767
--- /dev/null
+++ b/devstack/tests/test_localconf_extract.py
@@ -0,0 +1,71 @@
1# Copyright 2017 IBM
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15# Implementation of ini add / remove for devstack. We don't use the
16# python ConfigFile parser because that ends up rewriting the entire
17# file and doesn't ensure comments remain.
18
19import fixtures
20import os.path
21import testtools
22
23from devstack import dsconf
24
25
26BASIC = """
27[[local|localrc]]
28a = b
29c = d
30f = 1
31[[post-config|$NEUTRON_CONF]]
32[DEFAULT]
33global_physnet_mtu=1450
34[[post-config|$NOVA_CONF]]
35[upgrade_levels]
36compute = auto
37"""
38
39NOVA = """[upgrade_levels]
40compute = auto
41"""
42
43NEUTRON = """[DEFAULT]
44global_physnet_mtu = 1450
45"""
46
47
48class TestLcExtract(testtools.TestCase):
49
50 def setUp(self):
51 super(TestLcExtract, self).setUp()
52 self._path = self.useFixture(fixtures.TempDir()).path
53 self._path += "/local.conf"
54 with open(self._path, "w") as f:
55 f.write(BASIC)
56
57 def test_extract_neutron(self):
58 dirname = self.useFixture(fixtures.TempDir()).path
59 neutron = os.path.join(dirname, "neutron.conf")
60 nova = os.path.join(dirname, "nova.conf")
61 conf = dsconf.LocalConf(self._path)
62 conf.extract("post-config", "$NEUTRON_CONF", neutron)
63 conf.extract("post-config", "$NOVA_CONF", nova)
64
65 with open(neutron) as f:
66 content = f.read()
67 self.assertEqual(content, NEUTRON)
68
69 with open(nova) as f:
70 content = f.read()
71 self.assertEqual(content, NOVA)