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
|
os-apply-config
|
||||||
===============
|
===============
|
||||||
|
|
||||||
Apply configuration from cloud metadata (JSON).
|
Collect configuration from cloud metadata sources.
|
||||||
|
|
||||||
|
|
||||||
# What does it do?
|
# What does it do?
|
||||||
|
|
||||||
It turns a cloud-metadata file like this:
|
It collects data from defined configuration sources and runs a defined hook whenever the metadata has changed.
|
||||||
```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...
|
|
||||||
```
|
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
Just pass it the path to a directory tree of templates:
|
You must define what sources to collect configuration data from in /etc/os-collect-config/sources.ini
|
||||||
```
|
|
||||||
sudo os-apply-config -t /home/me/my_templates
|
The format of this file is
|
||||||
|
```ini
|
||||||
|
[default]
|
||||||
|
command=os-refresh-config
|
||||||
|
|
||||||
|
[ec2]
|
||||||
|
type=ec2-metadata
|
||||||
|
|
||||||
|
[cfn]
|
||||||
|
type=cloudformation
|
||||||
```
|
```
|
||||||
|
|
||||||
# Templates
|
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:
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
[sql]
|
/var/run/os-collect-config/ec2.json:/var/run/os-collect-config/cfn.json
|
||||||
connection = mysql://{{keystone.database.user}}:{{keystone.database.password}@{{keystone.database.host}}/keystone
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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"
|
|
||||||
```
|
```
|
||||||
|
os-collect-config --command=os-refresh-config --source ec2:type=ec2-metadata --source cfn:type=cloudformation
|
||||||
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)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# Quick Start
|
# Quick Start
|
||||||
```bash
|
|
||||||
# install it
|
|
||||||
sudo pip install -U git+git://github.com/stackforge/os-config-applier.git
|
|
||||||
|
|
||||||
# grab example templates
|
sudo pip install -U git+git://github.com/stackforge/os-collect-config.git
|
||||||
git clone git://github.com/stackforge/triple-image-elements /tmp/config
|
|
||||||
|
|
||||||
# run it
|
# run it on an OpenStack instance with access to ec2 metadata:
|
||||||
os-apply-config -t /tmp/config/elements/nova/os-config-applier/ -m /tmp/config/elements/boot-stack/config.json -o /tmp/config_output
|
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)
|
ec2_metadata[item] = _fetch_metadata('/%s' % item)
|
||||||
return ec2_metadata
|
return ec2_metadata
|
||||||
|
|
||||||
|
|
||||||
def __main__():
|
def __main__():
|
||||||
print json.dumps(collect_ec2())
|
print json.dumps(collect_ec2())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
__main__()
|
__main__()
|
@ -3,4 +3,3 @@ argparse
|
|||||||
d2to1
|
d2to1
|
||||||
httplib2
|
httplib2
|
||||||
pbr
|
pbr
|
||||||
pystache
|
|
||||||
|
12
setup.cfg
12
setup.cfg
@ -1,11 +1,11 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
name = os-apply-config
|
name = os-collect-config
|
||||||
author = OpenStack
|
author = OpenStack
|
||||||
author-email = openstack-dev@lists.openstack.org
|
author-email = openstack-dev@lists.openstack.org
|
||||||
summary = Config files from cloud metadata
|
summary = Collect and cache metadata, run hooks on changes.
|
||||||
description-file =
|
description-file =
|
||||||
README.md
|
README.md
|
||||||
home-page = http://github.com/stackforge/os-config-applier
|
home-page = http://github.com/stackforge/os-collect-config
|
||||||
classifier =
|
classifier =
|
||||||
Development Status :: 4 - Beta
|
Development Status :: 4 - Beta
|
||||||
Environment :: Console
|
Environment :: Console
|
||||||
@ -18,7 +18,7 @@ classifier =
|
|||||||
|
|
||||||
[files]
|
[files]
|
||||||
packages =
|
packages =
|
||||||
os_apply_config
|
os_collect_config
|
||||||
|
|
||||||
[global]
|
[global]
|
||||||
setup-hooks =
|
setup-hooks =
|
||||||
@ -26,9 +26,7 @@ setup-hooks =
|
|||||||
|
|
||||||
[entry_points]
|
[entry_points]
|
||||||
console_scripts =
|
console_scripts =
|
||||||
os-config-applier = os_apply_config.os_apply_config:main
|
os-collect-config = os_collect_config.collect:__main__
|
||||||
os-apply-config = os_apply_config.os_apply_config:main
|
|
||||||
os-collect-config = os_apply_config.collect:__main__
|
|
||||||
|
|
||||||
[egg_info]
|
[egg_info]
|
||||||
tag_build =
|
tag_build =
|
||||||
|
Loading…
Reference in New Issue
Block a user