Splitting os-collect-config into its own repo.
This commit is contained in:
parent
5841dd6e24
commit
05b667a977
105
README.md
105
README.md
@ -1,106 +1,47 @@
|
||||
os-apply-config
|
||||
===============
|
||||
|
||||
Apply configuration from cloud metadata (JSON).
|
||||
Collect configuration from cloud metadata sources.
|
||||
|
||||
|
||||
# What does it do?
|
||||
|
||||
It turns a cloud-metadata file like this:
|
||||
```javascript
|
||||
{"keystone": {"database": {"host": "127.0.0.1", "user": "keystone", "password": "foobar"}}}
|
||||
```
|
||||
into service config files like this:
|
||||
```
|
||||
[sql]
|
||||
connection = mysql://keystone:foobar@127.0.0.1/keystone
|
||||
...other settings...
|
||||
```
|
||||
It collects data from defined configuration sources and runs a defined hook whenever the metadata has changed.
|
||||
|
||||
# Usage
|
||||
|
||||
Just pass it the path to a directory tree of templates:
|
||||
```
|
||||
sudo os-apply-config -t /home/me/my_templates
|
||||
You must define what sources to collect configuration data from in /etc/os-collect-config/sources.ini
|
||||
|
||||
The format of this file is
|
||||
```ini
|
||||
[default]
|
||||
command=os-refresh-config
|
||||
|
||||
[ec2]
|
||||
type=ec2-metadata
|
||||
|
||||
[cfn]
|
||||
type=cloudformation
|
||||
```
|
||||
|
||||
# Templates
|
||||
|
||||
The template directory structure should mimic a root filesystem, and contain templates for only those files you want configured.
|
||||
|
||||
e.g.
|
||||
```
|
||||
~/my_templates$ tree
|
||||
.
|
||||
└── etc
|
||||
├── keystone
|
||||
│ └── keystone.conf
|
||||
└── mysql
|
||||
└── mysql.conf
|
||||
```
|
||||
|
||||
An example tree [can be found here](https://github.com/tripleo/openstack_config_templates).
|
||||
|
||||
If a template is executable it will be treated as an **executable template**.
|
||||
Otherwise, it will be treated as a **mustache template**.
|
||||
|
||||
## Mustache Templates
|
||||
|
||||
If you don't need any logic, just some string substitution, use a mustache template.
|
||||
|
||||
Metadata settings are accessed with dot ('.') notation:
|
||||
These sources will be processed in order, and whenever any of them changes, default.command will be run. OS_CONFIG_FILES will be set in the environment as a colon (":") separated list of the current copy of each metadata source. So in the example above, "os-refresh-config" would be executed with something like this in OS_CONFIG_FILES:
|
||||
|
||||
```
|
||||
[sql]
|
||||
connection = mysql://{{keystone.database.user}}:{{keystone.database.password}@{{keystone.database.host}}/keystone
|
||||
/var/run/os-collect-config/ec2.json:/var/run/os-collect-config/cfn.json
|
||||
```
|
||||
|
||||
## Executable Templates
|
||||
The sources can also be crafted using runtime arguments:
|
||||
|
||||
Configuration requiring logic is expressed in executable templates.
|
||||
|
||||
An executable template is a script which accepts configuration as a JSON string on standard in, and writes a config file to standard out.
|
||||
|
||||
The script should exit non-zero if it encounters a problem, so that os-apply-config knows what's up.
|
||||
|
||||
The output of the script will be written to the path corresponding to the executable template's path in the template tree.
|
||||
|
||||
|
||||
```ruby
|
||||
#!/usr/bin/env ruby
|
||||
require 'json'
|
||||
params = JSON.parse STDIN.read
|
||||
puts "connection = mysql://#{c['keystone']['database']['user']}:#{c['keystone']['database']['password']}@#{c['keystone']['database']['host']}/keystone"
|
||||
```
|
||||
|
||||
You could even embed mustache in a heredoc, and use that:
|
||||
```ruby
|
||||
#!/usr/bin/env ruby
|
||||
require 'json'
|
||||
require 'mustache'
|
||||
params = JSON.parse STDIN.read
|
||||
|
||||
template = <<-eos
|
||||
[sql]
|
||||
connection = mysql://{{keystone.database.user}}:{{keystone.database.password}}@{{keystone.database.host}}/keystone
|
||||
|
||||
[log]
|
||||
...
|
||||
eos
|
||||
|
||||
# tweak params here...
|
||||
|
||||
puts Mustache.render(template, params)
|
||||
os-collect-config --command=os-refresh-config --source ec2:type=ec2-metadata --source cfn:type=cloudformation
|
||||
```
|
||||
|
||||
# Quick Start
|
||||
```bash
|
||||
# install it
|
||||
sudo pip install -U git+git://github.com/stackforge/os-config-applier.git
|
||||
|
||||
# grab example templates
|
||||
git clone git://github.com/stackforge/triple-image-elements /tmp/config
|
||||
sudo pip install -U git+git://github.com/stackforge/os-collect-config.git
|
||||
|
||||
# run it
|
||||
os-apply-config -t /tmp/config/elements/nova/os-config-applier/ -m /tmp/config/elements/boot-stack/config.json -o /tmp/config_output
|
||||
# run it on an OpenStack instance with access to ec2 metadata:
|
||||
os-collect-config --print --source "ec2:ec2-metadata"
|
||||
```
|
||||
|
||||
That should print out a json representation of the entire ec2 metadata tree.
|
||||
|
@ -1,18 +0,0 @@
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
class ConfigException(Exception):
|
||||
pass
|
@ -1,238 +0,0 @@
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from pystache import context
|
||||
|
||||
from config_exception import ConfigException
|
||||
from renderers import JsonRenderer
|
||||
from value_types import ensure_type
|
||||
|
||||
TEMPLATES_DIR = os.environ.get('OS_CONFIG_APPLIER_TEMPLATES', None)
|
||||
if TEMPLATES_DIR is None:
|
||||
TEMPLATES_DIR = '/opt/stack/os-apply-config/templates'
|
||||
if not os.path.isdir(TEMPLATES_DIR):
|
||||
# Backwards compat with the old name.
|
||||
TEMPLATES_DIR = '/opt/stack/os-config-applier/templates'
|
||||
|
||||
|
||||
def install_config(config_path, template_root,
|
||||
output_path, validate, subhash=None):
|
||||
config = strip_hash(read_config(config_path), subhash)
|
||||
tree = build_tree(template_paths(template_root), config)
|
||||
if not validate:
|
||||
for path, contents in tree.items():
|
||||
write_file(os.path.join(
|
||||
output_path, strip_prefix('/', path)), contents)
|
||||
|
||||
|
||||
def print_key(config_path, key, type_name, default=None):
|
||||
config = read_config(config_path)
|
||||
keys = key.split('.')
|
||||
for key in keys:
|
||||
try:
|
||||
config = config[key]
|
||||
except KeyError:
|
||||
if default is not None:
|
||||
print default
|
||||
return
|
||||
else:
|
||||
raise ConfigException(
|
||||
'key %s does not exist in %s' % (key, config_path))
|
||||
ensure_type(config, type_name)
|
||||
print config
|
||||
|
||||
|
||||
def write_file(path, contents):
|
||||
logger.info("writing %s", path)
|
||||
d = os.path.dirname(path)
|
||||
os.path.exists(d) or os.makedirs(d)
|
||||
with tempfile.NamedTemporaryFile(dir=d, delete=False) as newfile:
|
||||
newfile.write(contents)
|
||||
os.chmod(newfile.name, 0644)
|
||||
os.rename(newfile.name, path)
|
||||
|
||||
# return a map of filenames->filecontents
|
||||
|
||||
|
||||
def build_tree(templates, config):
|
||||
res = {}
|
||||
for in_file, out_file in templates:
|
||||
res[out_file] = render_template(in_file, config)
|
||||
return res
|
||||
|
||||
|
||||
def render_template(template, config):
|
||||
if is_executable(template):
|
||||
return render_executable(template, config)
|
||||
else:
|
||||
try:
|
||||
return render_moustache(open(template).read(), config)
|
||||
except context.KeyNotFoundError as e:
|
||||
raise ConfigException(
|
||||
"key '%s' from template '%s' does not exist in metadata file."
|
||||
% (e.key, template))
|
||||
except Exception as e:
|
||||
raise ConfigException(
|
||||
"could not render moustache template %s" % template)
|
||||
|
||||
|
||||
def is_executable(path):
|
||||
return os.path.isfile(path) and os.access(path, os.X_OK)
|
||||
|
||||
|
||||
def render_moustache(text, config):
|
||||
r = JsonRenderer(missing_tags='ignore')
|
||||
return r.render(text, config)
|
||||
|
||||
|
||||
def render_executable(path, config):
|
||||
p = subprocess.Popen([path],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
stdout, stderr = p.communicate(json.dumps(config))
|
||||
p.wait()
|
||||
if p.returncode != 0:
|
||||
raise ConfigException(
|
||||
"config script failed: %s\n\nwith output:\n\n%s" %
|
||||
(path, stdout + stderr))
|
||||
return stdout
|
||||
|
||||
|
||||
def read_config(paths):
|
||||
for path in paths:
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
return json.loads(open(path).read())
|
||||
except Exception:
|
||||
raise ConfigException("invalid metadata file: %s" % path)
|
||||
raise ConfigException("No metadata found.")
|
||||
|
||||
|
||||
def template_paths(root):
|
||||
res = []
|
||||
for cur_root, subdirs, files in os.walk(root):
|
||||
for f in files:
|
||||
inout = (os.path.join(cur_root, f), os.path.join(
|
||||
strip_prefix(root, cur_root), f))
|
||||
res.append(inout)
|
||||
return res
|
||||
|
||||
|
||||
def strip_prefix(prefix, s):
|
||||
return s[len(prefix):] if s.startswith(prefix) else s
|
||||
|
||||
|
||||
def strip_hash(h, keys):
|
||||
if not keys:
|
||||
return h
|
||||
for k in keys.split('.'):
|
||||
if k in h and isinstance(h[k], dict):
|
||||
h = h[k]
|
||||
else:
|
||||
raise ConfigException(
|
||||
"key '%s' does not correspond to a hash in the metadata file"
|
||||
% keys)
|
||||
return h
|
||||
|
||||
|
||||
def parse_opts(argv):
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-t', '--templates', metavar='TEMPLATE_ROOT',
|
||||
help="""path to template root directory (default:
|
||||
%(default)s)""",
|
||||
default=TEMPLATES_DIR)
|
||||
parser.add_argument('-o', '--output', metavar='OUT_DIR',
|
||||
help='root directory for output (default:%(default)s)',
|
||||
default='/')
|
||||
parser.add_argument('-m', '--metadata', metavar='METADATA_FILE', nargs='*',
|
||||
help='path to metadata files. First one that exists'
|
||||
' will be used. (default: %(default)s)',
|
||||
default=['/var/cache/heat-cfntools/last_metadata',
|
||||
'/var/lib/heat-cfntools/cfn-init-data',
|
||||
'/var/lib/cloud/data/cfn-init-data'])
|
||||
parser.add_argument(
|
||||
'-v', '--validate', help='validate only. do not write files',
|
||||
default=False, action='store_true')
|
||||
parser.add_argument(
|
||||
'--print-templates', default=False, action='store_true',
|
||||
help='Print templates root and exit.')
|
||||
parser.add_argument('-s', '--subhash',
|
||||
help='use the sub-hash named by this key,'
|
||||
' instead of the full metadata hash')
|
||||
parser.add_argument('--key', metavar='KEY', default=None,
|
||||
help='print the specified key and exit.'
|
||||
' (may be used with --type and --key-default)')
|
||||
parser.add_argument('--type', default='default',
|
||||
help='exit with error if the specified --key does not'
|
||||
' match type. Valid types are'
|
||||
' <int|default|netaddress|raw>')
|
||||
parser.add_argument('--key-default',
|
||||
help='This option only affects running with --key.'
|
||||
' Print this if key is not found. This value is'
|
||||
' not subject to type restrictions. If --key is'
|
||||
' specified and no default is specified, program'
|
||||
' exits with an error on missing key.')
|
||||
opts = parser.parse_args(argv[1:])
|
||||
|
||||
return opts
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
opts = parse_opts(argv)
|
||||
if opts.print_templates:
|
||||
print(opts.templates)
|
||||
return 0
|
||||
|
||||
try:
|
||||
if opts.templates is None:
|
||||
raise ConfigException('missing option --templates')
|
||||
|
||||
if opts.key:
|
||||
print_key(opts.metadata,
|
||||
opts.key,
|
||||
opts.type,
|
||||
opts.key_default)
|
||||
else:
|
||||
install_config(opts.metadata, opts.templates, opts.output,
|
||||
opts.validate, opts.subhash)
|
||||
logger.info("success")
|
||||
except ConfigException as e:
|
||||
logger.error(e)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
# logging
|
||||
LOG_FORMAT = '[%(asctime)s] [%(levelname)s] %(message)s'
|
||||
DATE_FORMAT = '%Y/%m/%d %I:%M:%S %p'
|
||||
|
||||
|
||||
def add_handler(logger, handler):
|
||||
handler.setFormatter(logging.Formatter(LOG_FORMAT, datefmt=DATE_FORMAT))
|
||||
logger.addHandler(handler)
|
||||
logger = logging.getLogger('os-apply-config')
|
||||
logger.setLevel(logging.INFO)
|
||||
add_handler(logger, logging.StreamHandler())
|
||||
if os.geteuid() == 0:
|
||||
add_handler(logger, logging.FileHandler('/var/log/os-apply-config.log'))
|
@ -1,41 +0,0 @@
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import json
|
||||
|
||||
import pystache
|
||||
|
||||
|
||||
class JsonRenderer(pystache.Renderer):
|
||||
def __init__(self,
|
||||
file_encoding=None,
|
||||
string_encoding=None,
|
||||
decode_errors=None,
|
||||
search_dirs=None,
|
||||
file_extension=None,
|
||||
escape=None,
|
||||
partials=None,
|
||||
missing_tags=None):
|
||||
# json would be html escaped otherwise
|
||||
if escape is None:
|
||||
escape = lambda u: u
|
||||
return super(JsonRenderer, self).__init__(file_encoding,
|
||||
string_encoding,
|
||||
decode_errors, search_dirs,
|
||||
file_extension, escape,
|
||||
partials, missing_tags)
|
||||
|
||||
def str_coerce(self, val):
|
||||
return json.dumps(val)
|
@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import json
|
||||
import sys
|
||||
params = json.loads(sys.stdin.read())
|
||||
x = params["x"]
|
||||
if x is None: raise Exception("undefined: x")
|
||||
print x
|
@ -1,2 +0,0 @@
|
||||
[foo]
|
||||
database = {{database.url}}
|
@ -1,38 +0,0 @@
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import json
|
||||
|
||||
import testtools
|
||||
from testtools import content
|
||||
|
||||
from os_apply_config import renderers
|
||||
|
||||
TEST_JSON = '{"a":{"b":[1,2,3,"foo"],"c": "the quick brown fox"}}'
|
||||
|
||||
|
||||
class JsonRendererTestCase(testtools.TestCase):
|
||||
|
||||
def test_json_renderer(self):
|
||||
context = json.loads(TEST_JSON)
|
||||
x = renderers.JsonRenderer()
|
||||
result = x.render('{{a.b}}', context)
|
||||
self.addDetail('result', content.text_content(result))
|
||||
result_structure = json.loads(result)
|
||||
desire_structure = json.loads('[1,2,3,"foo"]')
|
||||
self.assertEqual(desire_structure, result_structure)
|
||||
result = x.render('{{a.c}}', context)
|
||||
self.addDetail('result', content.text_content(result))
|
||||
self.assertEqual(u'the quick brown fox', result)
|
@ -1,254 +0,0 @@
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import fixtures
|
||||
import testtools
|
||||
|
||||
from os_apply_config import config_exception
|
||||
from os_apply_config import os_apply_config as oca
|
||||
|
||||
# example template tree
|
||||
TEMPLATES = os.path.join(os.path.dirname(__file__), 'templates')
|
||||
TEMPLATE_PATHS = [
|
||||
"/etc/glance/script.conf",
|
||||
"/etc/keystone/keystone.conf"
|
||||
]
|
||||
|
||||
# config for example tree
|
||||
CONFIG = {
|
||||
"x": "foo",
|
||||
"database": {
|
||||
"url": "sqlite:///blah"
|
||||
}
|
||||
}
|
||||
|
||||
# config for example tree - with subhash
|
||||
CONFIG_SUBHASH = {
|
||||
"OpenStack::Config": {
|
||||
"x": "foo",
|
||||
"database": {
|
||||
"url": "sqlite:///blah"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# expected output for example tree
|
||||
OUTPUT = {
|
||||
"/etc/glance/script.conf": "foo\n",
|
||||
"/etc/keystone/keystone.conf": "[foo]\ndatabase = sqlite:///blah\n"
|
||||
}
|
||||
|
||||
|
||||
def main_path():
|
||||
return (
|
||||
os.path.dirname(os.path.realpath(__file__)) +
|
||||
'/../os_apply_config.py')
|
||||
|
||||
|
||||
def template(relpath):
|
||||
return os.path.join(TEMPLATES, relpath[1:])
|
||||
|
||||
|
||||
class TestRunOSConfigApplier(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestRunOSConfigApplier, self).setUp()
|
||||
self.useFixture(fixtures.NestedTempfile())
|
||||
self.stdout = self.useFixture(fixtures.StringStream('stdout')).stream
|
||||
self.useFixture(fixtures.MonkeyPatch('sys.stdout', self.stdout))
|
||||
stderr = self.useFixture(fixtures.StringStream('stderr')).stream
|
||||
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
|
||||
self.logger = self.useFixture(
|
||||
fixtures.FakeLogger(name="os-apply-config"))
|
||||
fd, self.path = tempfile.mkstemp()
|
||||
with os.fdopen(fd, 'w') as t:
|
||||
t.write(json.dumps(CONFIG))
|
||||
t.flush()
|
||||
|
||||
def test_print_key(self):
|
||||
self.assertEqual(0, oca.main(
|
||||
['os-apply-config.py', '--metadata', self.path, '--key',
|
||||
'database.url', '--type', 'raw']))
|
||||
self.stdout.seek(0)
|
||||
self.assertEqual(CONFIG['database']['url'],
|
||||
self.stdout.read().strip())
|
||||
self.assertEqual('', self.logger.output)
|
||||
|
||||
def test_print_key_missing(self):
|
||||
self.assertEqual(1, oca.main(
|
||||
['os-apply-config.py', '--metadata', self.path, '--key',
|
||||
'does.not.exist']))
|
||||
self.assertIn('does not exist', self.logger.output)
|
||||
|
||||
def test_print_key_missing_default(self):
|
||||
self.assertEqual(0, oca.main(
|
||||
['os-apply-config.py', '--metadata', self.path, '--key',
|
||||
'does.not.exist', '--key-default', '']))
|
||||
self.stdout.seek(0)
|
||||
self.assertEqual('', self.stdout.read().strip())
|
||||
self.assertEqual('', self.logger.output)
|
||||
|
||||
def test_print_key_wrong_type(self):
|
||||
self.assertEqual(1, oca.main(
|
||||
['os-apply-config.py', '--metadata', self.path, '--key',
|
||||
'x', '--type', 'int']))
|
||||
self.assertIn('cannot interpret value', self.logger.output)
|
||||
|
||||
def test_print_templates(self):
|
||||
oca.main(['os-apply-config', '--print-templates'])
|
||||
self.stdout.seek(0)
|
||||
self.assertEqual(self.stdout.read().strip(), oca.TEMPLATES_DIR)
|
||||
self.assertEqual('', self.logger.output)
|
||||
|
||||
|
||||
class OSConfigApplierTestCase(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(OSConfigApplierTestCase, self).setUp()
|
||||
self.useFixture(fixtures.FakeLogger('os-apply-config'))
|
||||
self.useFixture(fixtures.NestedTempfile())
|
||||
|
||||
def test_install_config(self):
|
||||
fd, path = tempfile.mkstemp()
|
||||
with os.fdopen(fd, 'w') as t:
|
||||
t.write(json.dumps(CONFIG))
|
||||
t.flush()
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
oca.install_config([path], TEMPLATES, tmpdir, False)
|
||||
for path, contents in OUTPUT.items():
|
||||
full_path = os.path.join(tmpdir, path[1:])
|
||||
assert os.path.exists(full_path)
|
||||
self.assertEqual(open(full_path).read(), contents)
|
||||
|
||||
def test_install_config_subhash(self):
|
||||
fd, tpath = tempfile.mkstemp()
|
||||
with os.fdopen(fd, 'w') as t:
|
||||
t.write(json.dumps(CONFIG_SUBHASH))
|
||||
t.flush()
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
oca.install_config(
|
||||
[tpath], TEMPLATES, tmpdir, False, 'OpenStack::Config')
|
||||
for path, contents in OUTPUT.items():
|
||||
full_path = os.path.join(tmpdir, path[1:])
|
||||
assert os.path.exists(full_path)
|
||||
self.assertEqual(open(full_path).read(), contents)
|
||||
|
||||
def test_build_tree(self):
|
||||
self.assertEqual(oca.build_tree(
|
||||
oca.template_paths(TEMPLATES), CONFIG), OUTPUT)
|
||||
|
||||
def test_render_template(self):
|
||||
# execute executable files, moustache non-executables
|
||||
self.assertEqual(oca.render_template(template(
|
||||
"/etc/glance/script.conf"), {"x": "abc"}), "abc\n")
|
||||
self.assertRaises(
|
||||
config_exception.ConfigException, oca.render_template, template(
|
||||
"/etc/glance/script.conf"), {})
|
||||
|
||||
def test_render_moustache(self):
|
||||
self.assertEqual(oca.render_moustache("ab{{x.a}}cd", {
|
||||
"x": {"a": "123"}}), "ab123cd")
|
||||
|
||||
def test_render_moustache_bad_key(self):
|
||||
self.assertEqual(oca.render_moustache("{{badkey}}", {}), u'')
|
||||
|
||||
def test_render_executable(self):
|
||||
params = {"x": "foo"}
|
||||
self.assertEqual(oca.render_executable(template(
|
||||
"/etc/glance/script.conf"), params), "foo\n")
|
||||
|
||||
def test_render_executable_failure(self):
|
||||
self.assertRaises(
|
||||
config_exception.ConfigException,
|
||||
oca.render_executable, template("/etc/glance/script.conf"), {})
|
||||
|
||||
def test_template_paths(self):
|
||||
expected = map(lambda p: (template(p), p), TEMPLATE_PATHS)
|
||||
actual = oca.template_paths(TEMPLATES)
|
||||
expected.sort(key=lambda tup: tup[1])
|
||||
actual.sort(key=lambda tup: tup[1])
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_read_config(self):
|
||||
with tempfile.NamedTemporaryFile() as t:
|
||||
d = {"a": {"b": ["c", "d"]}}
|
||||
t.write(json.dumps(d))
|
||||
t.flush()
|
||||
self.assertEqual(oca.read_config([t.name]), d)
|
||||
|
||||
def test_read_config_bad_json(self):
|
||||
with tempfile.NamedTemporaryFile() as t:
|
||||
t.write("{{{{")
|
||||
t.flush()
|
||||
self.assertRaises(config_exception.ConfigException,
|
||||
oca.read_config, [t.name])
|
||||
|
||||
def test_read_config_no_file(self):
|
||||
self.assertRaises(config_exception.ConfigException,
|
||||
oca.read_config, ["/nosuchfile"])
|
||||
|
||||
def test_read_config_multi(self):
|
||||
with tempfile.NamedTemporaryFile(mode='wb') as t1:
|
||||
with tempfile.NamedTemporaryFile(mode='wb') as t2:
|
||||
d1 = {"a": {"b": [1, 2]}}
|
||||
d2 = {"x": {"y": [8, 9]}}
|
||||
t1.write(json.dumps(d1))
|
||||
t1.flush()
|
||||
t2.write(json.dumps(d2))
|
||||
t2.flush()
|
||||
result = oca.read_config([t1.name, t2.name])
|
||||
self.assertEqual(d1, result)
|
||||
|
||||
def test_read_config_multi_missing1(self):
|
||||
with tempfile.NamedTemporaryFile(mode='wb') as t1:
|
||||
pass
|
||||
with tempfile.NamedTemporaryFile(mode='wb') as t2:
|
||||
d2 = {"x": {"y": [8, 9]}}
|
||||
t2.write(json.dumps(d2))
|
||||
t2.flush()
|
||||
result = oca.read_config([t1.name, t2.name])
|
||||
self.assertEqual(d2, result)
|
||||
|
||||
def test_read_config_multi_missing_bad1(self):
|
||||
with tempfile.NamedTemporaryFile(mode='wb') as t1:
|
||||
t1.write('{{{')
|
||||
t1.flush()
|
||||
with tempfile.NamedTemporaryFile(mode='wb') as t2:
|
||||
pass
|
||||
d2 = {"x": {"y": [8, 9]}}
|
||||
t2.write(json.dumps(d2))
|
||||
t2.flush()
|
||||
self.assertRaises(config_exception.ConfigException,
|
||||
oca.read_config, [t1.name, t2.name])
|
||||
|
||||
def test_read_config_multi_missing_all(self):
|
||||
with tempfile.NamedTemporaryFile(mode='wb') as t1:
|
||||
pass
|
||||
with tempfile.NamedTemporaryFile(mode='wb') as t2:
|
||||
pass
|
||||
self.assertRaises(config_exception.ConfigException,
|
||||
oca.read_config, [t1.name, t2.name])
|
||||
|
||||
def test_strip_hash(self):
|
||||
h = {'a': {'b': {'x': 'y'}}, "c": [1, 2, 3]}
|
||||
self.assertEqual(oca.strip_hash(h, 'a.b'), {'x': 'y'})
|
||||
self.assertRaises(config_exception.ConfigException,
|
||||
oca.strip_hash, h, 'a.nonexistent')
|
||||
self.assertRaises(config_exception.ConfigException,
|
||||
oca.strip_hash, h, 'a.c')
|
@ -1,69 +0,0 @@
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import testtools
|
||||
|
||||
from os_apply_config import config_exception
|
||||
from os_apply_config import value_types
|
||||
|
||||
|
||||
class ValueTypeTestCase(testtools.TestCase):
|
||||
|
||||
def test_unknown_type(self):
|
||||
self.assertRaises(
|
||||
ValueError, value_types.ensure_type, "foo", "badtype")
|
||||
|
||||
def test_int(self):
|
||||
self.assertEqual("123", value_types.ensure_type("123", "int"))
|
||||
|
||||
def test_default(self):
|
||||
self.assertEqual("foobar",
|
||||
value_types.ensure_type("foobar", "default"))
|
||||
self.assertEqual("x86_64",
|
||||
value_types.ensure_type("x86_64", "default"))
|
||||
|
||||
def test_default_bad(self):
|
||||
self.assertRaises(config_exception.ConfigException,
|
||||
value_types.ensure_type, "foo\nbar", "default")
|
||||
|
||||
def test_default_empty(self):
|
||||
self.assertEqual('',
|
||||
value_types.ensure_type('', 'default'))
|
||||
|
||||
def test_raw_empty(self):
|
||||
self.assertEqual('',
|
||||
value_types.ensure_type('', 'raw'))
|
||||
|
||||
def test_net_address_ipv4(self):
|
||||
self.assertEqual('192.0.2.1', value_types.ensure_type('192.0.2.1',
|
||||
'netaddress'))
|
||||
|
||||
def test_net_address_cidr(self):
|
||||
self.assertEqual('192.0.2.0/24',
|
||||
value_types.ensure_type('192.0.2.0/24', 'netaddress'))
|
||||
|
||||
def test_ent_address_ipv6(self):
|
||||
self.assertEqual('::', value_types.ensure_type('::', 'netaddress'))
|
||||
self.assertEqual('2001:db8::2:1', value_types.ensure_type(
|
||||
'2001:db8::2:1', 'netaddress'))
|
||||
|
||||
def test_net_address_dns(self):
|
||||
self.assertEqual('host.0domain-name.test',
|
||||
value_types.ensure_type('host.0domain-name.test',
|
||||
'netaddress'))
|
||||
|
||||
def test_net_address_bad(self):
|
||||
self.assertRaises(config_exception.ConfigException,
|
||||
value_types.ensure_type, "192.0.2.1;DROP TABLE foo")
|
@ -1,35 +0,0 @@
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import re
|
||||
|
||||
from config_exception import ConfigException
|
||||
|
||||
TYPES = {
|
||||
"int": "^[0-9]+$",
|
||||
"default": "^[A-Za-z0-9_]*$",
|
||||
"netaddress": "^[A-Za-z0-9/.:-]+$",
|
||||
"raw": ""
|
||||
}
|
||||
|
||||
|
||||
def ensure_type(string_value, type_name='default'):
|
||||
if type_name not in TYPES:
|
||||
raise ValueError(
|
||||
"requested validation of unknown type: %s" % type_name)
|
||||
if not re.match(TYPES[type_name], string_value):
|
||||
raise ConfigException("cannot interpret value '%s' as type %s" % (
|
||||
string_value, type_name))
|
||||
return string_value
|
@ -20,8 +20,10 @@ def collect_ec2():
|
||||
ec2_metadata[item] = _fetch_metadata('/%s' % item)
|
||||
return ec2_metadata
|
||||
|
||||
|
||||
def __main__():
|
||||
print json.dumps(collect_ec2())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
__main__()
|
@ -3,4 +3,3 @@ argparse
|
||||
d2to1
|
||||
httplib2
|
||||
pbr
|
||||
pystache
|
||||
|
12
setup.cfg
12
setup.cfg
@ -1,11 +1,11 @@
|
||||
[metadata]
|
||||
name = os-apply-config
|
||||
name = os-collect-config
|
||||
author = OpenStack
|
||||
author-email = openstack-dev@lists.openstack.org
|
||||
summary = Config files from cloud metadata
|
||||
summary = Collect and cache metadata, run hooks on changes.
|
||||
description-file =
|
||||
README.md
|
||||
home-page = http://github.com/stackforge/os-config-applier
|
||||
home-page = http://github.com/stackforge/os-collect-config
|
||||
classifier =
|
||||
Development Status :: 4 - Beta
|
||||
Environment :: Console
|
||||
@ -18,7 +18,7 @@ classifier =
|
||||
|
||||
[files]
|
||||
packages =
|
||||
os_apply_config
|
||||
os_collect_config
|
||||
|
||||
[global]
|
||||
setup-hooks =
|
||||
@ -26,9 +26,7 @@ setup-hooks =
|
||||
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
os-config-applier = os_apply_config.os_apply_config:main
|
||||
os-apply-config = os_apply_config.os_apply_config:main
|
||||
os-collect-config = os_apply_config.collect:__main__
|
||||
os-collect-config = os_collect_config.collect:__main__
|
||||
|
||||
[egg_info]
|
||||
tag_build =
|
||||
|
Loading…
Reference in New Issue
Block a user