Retire Packaging Deb project repos
This commit is part of a series to retire the Packaging Deb project. Step 2 is to remove all content from the project repos, replacing it with a README notification where to find ongoing work, and how to recover the repo if needed at some future point (as in https://docs.openstack.org/infra/manual/drivers.html#retiring-a-project). Change-Id: Ie925a35847b69cac5762e1bba205b1a1364c21a1
This commit is contained in:
parent
c2e15c8424
commit
cb4c456861
@ -1,7 +0,0 @@
|
||||
[run]
|
||||
branch = True
|
||||
source = os_apply_config
|
||||
omit = os_apply_config/tests/*,os_apply_config/openstack/*
|
||||
|
||||
[report]
|
||||
ignore_errors = True
|
45
.gitignore
vendored
45
.gitignore
vendored
@ -1,45 +0,0 @@
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Packages
|
||||
*.egg
|
||||
*.egg-info
|
||||
dist
|
||||
build
|
||||
eggs
|
||||
parts
|
||||
bin
|
||||
var
|
||||
sdist
|
||||
develop-eggs
|
||||
.installed.cfg
|
||||
lib
|
||||
lib64
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
.coverage
|
||||
cover
|
||||
.testrepository
|
||||
.tox
|
||||
nosetests.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
# Mr Developer
|
||||
.mr.developer.cfg
|
||||
.project
|
||||
.pydevproject
|
||||
|
||||
# OpenStack Generated Files
|
||||
AUTHORS
|
||||
ChangeLog
|
||||
|
||||
# Editors
|
||||
*~
|
||||
*.swp
|
@ -1,4 +0,0 @@
|
||||
[gerrit]
|
||||
host=review.openstack.org
|
||||
port=29418
|
||||
project=openstack/os-apply-config.git
|
@ -1,4 +0,0 @@
|
||||
[DEFAULT]
|
||||
test_command=${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION
|
||||
test_id_option=--load-list $IDFILE
|
||||
test_list_option=--list
|
176
LICENSE
176
LICENSE
@ -1,176 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
@ -1,7 +0,0 @@
|
||||
include AUTHORS
|
||||
include ChangeLog
|
||||
include README.md
|
||||
exclude .gitignore
|
||||
exclude .gitreview
|
||||
|
||||
global-exclude *.pyc
|
14
README
Normal file
14
README
Normal file
@ -0,0 +1,14 @@
|
||||
This project is no longer maintained.
|
||||
|
||||
The contents of this repository are still available in the Git
|
||||
source code management system. To see the contents of this
|
||||
repository before it reached its end of life, please check out the
|
||||
previous commit with "git checkout HEAD^1".
|
||||
|
||||
For ongoing work on maintaining OpenStack packages in the Debian
|
||||
distribution, please see the Debian OpenStack packaging team at
|
||||
https://wiki.debian.org/OpenStack/.
|
||||
|
||||
For any further questions, please email
|
||||
openstack-dev@lists.openstack.org or join #openstack-dev on
|
||||
Freenode.
|
148
README.rst
148
README.rst
@ -1,148 +0,0 @@
|
||||
========================
|
||||
Team and repository tags
|
||||
========================
|
||||
|
||||
.. image:: http://governance.openstack.org/badges/os-apply-config.svg
|
||||
:target: http://governance.openstack.org/reference/tags/index.html
|
||||
|
||||
.. Change things from this point on
|
||||
|
||||
===============
|
||||
os-apply-config
|
||||
===============
|
||||
|
||||
-----------------------------------------------
|
||||
Apply configuration from cloud metadata (JSON)
|
||||
-----------------------------------------------
|
||||
|
||||
What does it do?
|
||||
================
|
||||
|
||||
It turns metadata from one or more JSON files like this::
|
||||
|
||||
{"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
|
||||
=====
|
||||
|
||||
Just pass it the path to a directory tree of templates::
|
||||
|
||||
sudo os-apply-config -t /home/me/my_templates
|
||||
|
||||
By default it will read config files according to the contents of
|
||||
the file `/var/lib/os-collect-config/os_config_files.json`. In
|
||||
order to remain backward compatible it will also fall back to
|
||||
/var/run/os-collect-config/os_config_files.json, but the fallback
|
||||
path is deprecated and will be removed in a later release. The main
|
||||
path can be changed with the command line switch `--os-config-files`,
|
||||
or the environment variable `OS_CONFIG_FILES_PATH`. The list can
|
||||
also be overridden with the environment variable `OS_CONFIG_FILES`.
|
||||
If overriding with `OS_CONFIG_FILES`, the paths are expected to be colon,
|
||||
":", separated. Each json file referred to must have a mapping as their
|
||||
root structure. Keys in files mentioned later in the list will override
|
||||
keys in earlier files from this list. For example::
|
||||
|
||||
OS_CONFIG_FILES=/tmp/ec2.json:/tmp/cfn.json os-apply-config
|
||||
|
||||
This will read `ec2.json` and `cfn.json`, and if they have any
|
||||
overlapping keys, the value from `cfn.json` will be used. That will
|
||||
populate the tree for any templates found in the template path. See
|
||||
https://git.openstack.org/cgit/openstack/os-collect-config for a
|
||||
program that will automatically collect data and populate this list.
|
||||
|
||||
You can also override `OS_CONFIG_FILES` with the `--metadata` command
|
||||
line option, specifying it multiple times instead of colon separating
|
||||
the list.
|
||||
|
||||
`os-apply-config` will also always try to read metadata in the old
|
||||
legacy paths first to populate the tree. These paths can be changed
|
||||
with `--fallback-metadata`.
|
||||
|
||||
Templates
|
||||
=========
|
||||
|
||||
The template directory structure should mimic a root filesystem, and
|
||||
contain templates for only those files you want configured. For
|
||||
example::
|
||||
|
||||
~/my_templates$ tree
|
||||
.
|
||||
+-- etc
|
||||
+-- keystone
|
||||
| +-- keystone.conf
|
||||
+-- mysql
|
||||
+-- mysql.conf
|
||||
|
||||
An example tree can be found `here <http://git.openstack.org/cgit/openstack/tripleo-image-elements/tree/elements/keystone/os-apply-config>`_.
|
||||
|
||||
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]
|
||||
connection = mysql://{{keystone.database.user}}:{{keystone.database.password}}@{{keystone.database.host}}/keystone
|
||||
|
||||
Executable Templates
|
||||
--------------------
|
||||
|
||||
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::
|
||||
|
||||
#!/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::
|
||||
|
||||
#!/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
|
||||
===========
|
||||
::
|
||||
|
||||
# install it
|
||||
sudo pip install -U git+git://git.openstack.org/openstack/os-apply-config.git
|
||||
|
||||
# grab example templates
|
||||
git clone git://git.openstack.org/openstack/tripleo-image-elements /tmp/config
|
||||
|
||||
# run it
|
||||
os-apply-config -t /tmp/config/elements/nova/os-apply-config/ -m /tmp/config/elements/seed-stack-config/config.json -o /tmp/config_output
|
@ -1,385 +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
|
||||
import yaml
|
||||
|
||||
from os_apply_config import collect_config
|
||||
from os_apply_config import config_exception as exc
|
||||
from os_apply_config import oac_file
|
||||
from os_apply_config import renderers
|
||||
from os_apply_config import value_types
|
||||
from os_apply_config import version
|
||||
|
||||
DEFAULT_TEMPLATES_DIR = '/usr/libexec/os-apply-config/templates'
|
||||
|
||||
|
||||
def templates_dir():
|
||||
"""Determine the default templates directory path
|
||||
|
||||
If the OS_CONFIG_APPLIER_TEMPLATES environment variable has been set,
|
||||
use its value.
|
||||
Otherwise, select a default path based on which directories exist on the
|
||||
system, preferring the newer paths but still allowing the old ones for
|
||||
backwards compatibility.
|
||||
"""
|
||||
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'
|
||||
if (os.path.isdir(templates_dir) and
|
||||
not os.path.isdir(DEFAULT_TEMPLATES_DIR)):
|
||||
logging.warning('Template directory %s is deprecated. The '
|
||||
'recommended location for template files is %s',
|
||||
templates_dir, DEFAULT_TEMPLATES_DIR)
|
||||
else:
|
||||
templates_dir = DEFAULT_TEMPLATES_DIR
|
||||
return templates_dir
|
||||
|
||||
|
||||
TEMPLATES_DIR = templates_dir()
|
||||
OS_CONFIG_FILES_PATH = os.environ.get(
|
||||
'OS_CONFIG_FILES_PATH', '/var/lib/os-collect-config/os_config_files.json')
|
||||
OS_CONFIG_FILES_PATH_OLD = '/var/run/os-collect-config/os_config_files.json'
|
||||
|
||||
CONTROL_FILE_SUFFIX = ".oac"
|
||||
|
||||
|
||||
def install_config(
|
||||
config_path, template_root, output_path, validate, subhash=None,
|
||||
fallback_metadata=None):
|
||||
config = strip_hash(
|
||||
collect_config.collect_config(config_path, fallback_metadata), subhash)
|
||||
tree = build_tree(template_paths(template_root), config)
|
||||
if not validate:
|
||||
for path, obj in tree.items():
|
||||
write_file(os.path.join(
|
||||
output_path, strip_prefix('/', path)), obj)
|
||||
|
||||
|
||||
def _extract_key(config_path, key, fallback_metadata=None):
|
||||
config = collect_config.collect_config(config_path, fallback_metadata)
|
||||
keys = key.split('.')
|
||||
for key in keys:
|
||||
try:
|
||||
config = config[key]
|
||||
if config is None:
|
||||
raise TypeError()
|
||||
except (KeyError, TypeError):
|
||||
try:
|
||||
if type(config) == list:
|
||||
config = config[int(key)]
|
||||
continue
|
||||
except (IndexError, ValueError):
|
||||
pass
|
||||
return None
|
||||
return config
|
||||
|
||||
|
||||
def print_key(
|
||||
config_path, key, type_name, default=None, fallback_metadata=None):
|
||||
config = collect_config.collect_config(config_path, fallback_metadata)
|
||||
config = _extract_key(config_path, key, fallback_metadata)
|
||||
if config is None:
|
||||
if default is not None:
|
||||
print(str(default))
|
||||
return
|
||||
else:
|
||||
raise exc.ConfigException(
|
||||
'key %s does not exist in %s' % (key, config_path))
|
||||
value_types.ensure_type(str(config), type_name)
|
||||
if isinstance(config, (dict, list, bool)):
|
||||
print(json.dumps(config))
|
||||
else:
|
||||
print(str(config))
|
||||
|
||||
|
||||
def boolean_key(metadata, key, fallback_metadata):
|
||||
config = _extract_key(metadata, key, fallback_metadata)
|
||||
if not isinstance(config, bool):
|
||||
return -1
|
||||
if config:
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
|
||||
def write_file(path, obj):
|
||||
if not obj.allow_empty and len(obj.body) == 0:
|
||||
if os.path.exists(path):
|
||||
logger.info("deleting %s", path)
|
||||
os.unlink(path)
|
||||
else:
|
||||
logger.info("not creating empty %s", path)
|
||||
return
|
||||
|
||||
logger.info("writing %s", path)
|
||||
if os.path.exists(path):
|
||||
stat = os.stat(path)
|
||||
mode, uid, gid = stat.st_mode, stat.st_uid, stat.st_gid
|
||||
else:
|
||||
mode, uid, gid = 0o644, -1, -1
|
||||
mode = obj.mode or mode
|
||||
if obj.owner is not None:
|
||||
uid = obj.owner
|
||||
if obj.group is not None:
|
||||
gid = obj.group
|
||||
|
||||
d = os.path.dirname(path)
|
||||
os.path.exists(d) or os.makedirs(d)
|
||||
with tempfile.NamedTemporaryFile(dir=d, delete=False) as newfile:
|
||||
if type(obj.body) == str:
|
||||
obj.body = obj.body.encode('utf-8')
|
||||
newfile.write(obj.body)
|
||||
os.chmod(newfile.name, mode)
|
||||
os.chown(newfile.name, uid, gid)
|
||||
os.rename(newfile.name, path)
|
||||
|
||||
|
||||
def build_tree(templates, config):
|
||||
"""Return a map of filenames to OacFiles."""
|
||||
res = {}
|
||||
for in_file, out_file in templates:
|
||||
try:
|
||||
body = render_template(in_file, config)
|
||||
ctrl_file = in_file + CONTROL_FILE_SUFFIX
|
||||
ctrl_dict = {}
|
||||
if os.path.isfile(ctrl_file):
|
||||
with open(ctrl_file) as cf:
|
||||
ctrl_body = cf.read()
|
||||
ctrl_dict = yaml.safe_load(ctrl_body) or {}
|
||||
if not isinstance(ctrl_dict, dict):
|
||||
raise exc.ConfigException(
|
||||
"header is not a dict: %s" % in_file)
|
||||
res[out_file] = oac_file.OacFile(body, **ctrl_dict)
|
||||
except exc.ConfigException as e:
|
||||
e.args += in_file,
|
||||
raise
|
||||
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 exc.ConfigException(
|
||||
"key '%s' from template '%s' does not exist in metadata file."
|
||||
% (e.key, template))
|
||||
except Exception as e:
|
||||
logger.error("%s", e)
|
||||
raise exc.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 = renderers.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).encode('utf-8'))
|
||||
p.wait()
|
||||
if p.returncode != 0:
|
||||
raise exc.ConfigException(
|
||||
"config script failed: %s\n\nwith output:\n\n%s" %
|
||||
(path, stdout + stderr))
|
||||
return stdout.decode('utf-8')
|
||||
|
||||
|
||||
def template_paths(root):
|
||||
res = []
|
||||
for cur_root, _subdirs, files in os.walk(root):
|
||||
for f in files:
|
||||
if f.endswith(CONTROL_FILE_SUFFIX):
|
||||
continue
|
||||
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 exc.ConfigException(
|
||||
"key '%s' does not correspond to a hash in the metadata file"
|
||||
% keys)
|
||||
return h
|
||||
|
||||
|
||||
def parse_opts(argv):
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Reads and merges JSON configuration files specified'
|
||||
' by colon separated environment variable OS_CONFIG_FILES, unless'
|
||||
' overridden by command line option --metadata. If no files are'
|
||||
' specified this way, falls back to legacy behavior of searching'
|
||||
' the fallback metadata path for a single config file.')
|
||||
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='Overrides environment variable OS_CONFIG_FILES.'
|
||||
' Specify multiple times, rather than separate files'
|
||||
' with ":".',
|
||||
default=[])
|
||||
parser.add_argument('--fallback-metadata', metavar='FALLBACK_METADATA',
|
||||
nargs='*', help='Files to search when OS_CONFIG_FILES'
|
||||
' is empty. (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|netdevice|dsn|'
|
||||
'swiftdevices|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.')
|
||||
parser.add_argument('--boolean-key',
|
||||
help='This option is incompatible with --key.'
|
||||
' Use this to evaluate whether a value is'
|
||||
' boolean true or false. The return code of the'
|
||||
' command will be 0 for true, 1 for false, and -1'
|
||||
' for non-boolean values.')
|
||||
parser.add_argument('--version', action='version',
|
||||
version=version.version_info.version_string())
|
||||
parser.add_argument('--os-config-files',
|
||||
default=OS_CONFIG_FILES_PATH,
|
||||
help='Set path to os_config_files.json')
|
||||
opts = parser.parse_args(argv[1:])
|
||||
|
||||
return opts
|
||||
|
||||
|
||||
def load_list_from_json(json_file):
|
||||
json_obj = []
|
||||
if os.path.exists(json_file):
|
||||
with open(json_file) as ocf:
|
||||
json_obj = json.loads(ocf.read())
|
||||
if not isinstance(json_obj, list):
|
||||
raise ValueError("No list defined in json file: %s" % json_file)
|
||||
return json_obj
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
opts = parse_opts(argv)
|
||||
if opts.print_templates:
|
||||
print(opts.templates)
|
||||
return 0
|
||||
|
||||
if not opts.metadata:
|
||||
if 'OS_CONFIG_FILES' in os.environ:
|
||||
opts.metadata = os.environ['OS_CONFIG_FILES'].split(':')
|
||||
else:
|
||||
opts.metadata = load_list_from_json(opts.os_config_files)
|
||||
if ((not opts.metadata and opts.os_config_files ==
|
||||
OS_CONFIG_FILES_PATH)):
|
||||
logger.warning('DEPRECATED: falling back to %s' %
|
||||
OS_CONFIG_FILES_PATH_OLD)
|
||||
opts.metadata = load_list_from_json(OS_CONFIG_FILES_PATH_OLD)
|
||||
|
||||
if opts.key and opts.boolean_key:
|
||||
logger.warning('--key is not compatible with --boolean-key.'
|
||||
' --boolean-key ignored.')
|
||||
|
||||
try:
|
||||
if opts.templates is None:
|
||||
raise exc.ConfigException('missing option --templates')
|
||||
|
||||
if opts.key:
|
||||
print_key(opts.metadata,
|
||||
opts.key,
|
||||
opts.type,
|
||||
opts.key_default,
|
||||
opts.fallback_metadata)
|
||||
elif opts.boolean_key:
|
||||
return boolean_key(opts.metadata,
|
||||
opts.boolean_key,
|
||||
opts.fallback_metadata)
|
||||
else:
|
||||
install_config(opts.metadata, opts.templates, opts.output,
|
||||
opts.validate, opts.subhash, opts.fallback_metadata)
|
||||
logger.info("success")
|
||||
except exc.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'))
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv))
|
@ -1,70 +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 copy
|
||||
import json
|
||||
import os
|
||||
|
||||
from os_apply_config import config_exception as exc
|
||||
|
||||
|
||||
def read_configs(config_files):
|
||||
'''Generator yields data from any existing file in list config_files.'''
|
||||
for input_path in [x for x in config_files if x]:
|
||||
if os.path.exists(input_path):
|
||||
try:
|
||||
with open(input_path) as input_file:
|
||||
yield((input_file.read(), input_path))
|
||||
except IOError as e:
|
||||
raise exc.ConfigException('Could not open %s for reading. %s' %
|
||||
(input_path, e))
|
||||
|
||||
|
||||
def parse_configs(config_data):
|
||||
'''Generator yields parsed json for each item passed in config_data.'''
|
||||
for input_data, input_path in config_data:
|
||||
try:
|
||||
yield(json.loads(input_data))
|
||||
except ValueError:
|
||||
raise exc.ConfigException('Could not parse metadata file: %s' %
|
||||
input_path)
|
||||
|
||||
|
||||
def _deep_merge_dict(a, b):
|
||||
if not isinstance(b, dict):
|
||||
return b
|
||||
new_dict = copy.deepcopy(a)
|
||||
for k, v in iter(b.items()):
|
||||
if k in new_dict and isinstance(new_dict[k], dict):
|
||||
new_dict[k] = _deep_merge_dict(new_dict[k], v)
|
||||
else:
|
||||
new_dict[k] = copy.deepcopy(v)
|
||||
return new_dict
|
||||
|
||||
|
||||
def merge_configs(parsed_configs):
|
||||
'''Returns deep-merged dict from passed list of dicts.'''
|
||||
final_conf = {}
|
||||
for conf in parsed_configs:
|
||||
if conf:
|
||||
final_conf = _deep_merge_dict(final_conf, conf)
|
||||
return final_conf
|
||||
|
||||
|
||||
def collect_config(os_config_files, fallback_paths=None):
|
||||
'''Convenience method to read, parse, and merge all paths.'''
|
||||
if fallback_paths:
|
||||
os_config_files = fallback_paths + os_config_files
|
||||
return merge_configs(parse_configs(read_configs(os_config_files)))
|
@ -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,146 +0,0 @@
|
||||
# Copyright (c) 2014 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 grp
|
||||
import pwd
|
||||
|
||||
import six
|
||||
|
||||
from os_apply_config import config_exception as exc
|
||||
|
||||
|
||||
class OacFile(object):
|
||||
DEFAULTS = {
|
||||
'allow_empty': True,
|
||||
'mode': None,
|
||||
'owner': None,
|
||||
'group': None,
|
||||
}
|
||||
|
||||
def __init__(self, body, **kwargs):
|
||||
super(OacFile, self).__init__()
|
||||
self.body = body
|
||||
|
||||
for k, v in six.iteritems(self.DEFAULTS):
|
||||
setattr(self, '_' + k, v)
|
||||
|
||||
for k, v in six.iteritems(kwargs):
|
||||
if not hasattr(self, k):
|
||||
raise exc.ConfigException(
|
||||
"unrecognised file control key '%s'" % (k))
|
||||
setattr(self, k, v)
|
||||
|
||||
def __eq__(self, other):
|
||||
if type(other) is type(self):
|
||||
return self.__dict__ == other.__dict__
|
||||
return False
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
a = ["OacFile(%s" % repr(self.body)]
|
||||
for key, default in six.iteritems(self.DEFAULTS):
|
||||
value = getattr(self, key)
|
||||
if value != default:
|
||||
a.append("%s=%s" % (key, repr(value)))
|
||||
return ", ".join(a) + ")"
|
||||
|
||||
def set(self, key, value):
|
||||
"""Allows setting attrs as an expression rather than a statement."""
|
||||
setattr(self, key, value)
|
||||
return self
|
||||
|
||||
@property
|
||||
def allow_empty(self):
|
||||
"""Returns allow_empty.
|
||||
|
||||
If True and body='', no file will be created and any existing
|
||||
file will be deleted.
|
||||
"""
|
||||
return self._allow_empty
|
||||
|
||||
@allow_empty.setter
|
||||
def allow_empty(self, value):
|
||||
if type(value) is not bool:
|
||||
raise exc.ConfigException(
|
||||
"allow_empty requires Boolean, got: '%s'" % value)
|
||||
self._allow_empty = value
|
||||
return self
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
"""The permissions to set on the file, EG 0755."""
|
||||
return self._mode
|
||||
|
||||
@mode.setter
|
||||
def mode(self, v):
|
||||
"""Pass in the mode to set on the file.
|
||||
|
||||
EG 0644. Must be between 0 and 0777, the sticky bit is not supported.
|
||||
"""
|
||||
if type(v) is not int:
|
||||
raise exc.ConfigException("mode '%s' is not numeric" % v)
|
||||
if not 0 <= v <= 0o777:
|
||||
raise exc.ConfigException("mode '%#o' out of range" % v)
|
||||
self._mode = v
|
||||
|
||||
@property
|
||||
def owner(self):
|
||||
"""The UID to set on the file, EG 'rabbitmq' or '501'."""
|
||||
return self._owner
|
||||
|
||||
@owner.setter
|
||||
def owner(self, v):
|
||||
"""Pass in the UID to set on the file.
|
||||
|
||||
EG 'rabbitmq' or 501.
|
||||
"""
|
||||
try:
|
||||
if type(v) is int:
|
||||
user = pwd.getpwuid(v)
|
||||
elif type(v) is str:
|
||||
user = pwd.getpwnam(v)
|
||||
else:
|
||||
raise exc.ConfigException(
|
||||
"owner '%s' must be a string or int" % v)
|
||||
except KeyError:
|
||||
raise exc.ConfigException(
|
||||
"owner '%s' not found in passwd database" % v)
|
||||
self._owner = user[2]
|
||||
|
||||
@property
|
||||
def group(self):
|
||||
"""The GID to set on the file, EG 'rabbitmq' or '501'."""
|
||||
return self._group
|
||||
|
||||
@group.setter
|
||||
def group(self, v):
|
||||
"""Pass in the GID to set on the file.
|
||||
|
||||
EG 'rabbitmq' or 501.
|
||||
"""
|
||||
try:
|
||||
if type(v) is int:
|
||||
group = grp.getgrgid(v)
|
||||
elif type(v) is str:
|
||||
group = grp.getgrnam(v)
|
||||
else:
|
||||
raise exc.ConfigException(
|
||||
"group '%s' must be a string or int" % v)
|
||||
except KeyError:
|
||||
raise exc.ConfigException(
|
||||
"group '%s' not found in group database" % v)
|
||||
self._group = group[2]
|
@ -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 +0,0 @@
|
||||
lorem gido
|
@ -1 +0,0 @@
|
||||
group: 0
|
@ -1 +0,0 @@
|
||||
namo gido
|
@ -1 +0,0 @@
|
||||
group: root
|
@ -1 +0,0 @@
|
||||
namo uido
|
@ -1 +0,0 @@
|
||||
owner: root
|
@ -1 +0,0 @@
|
||||
lorem uido
|
@ -1 +0,0 @@
|
||||
owner: 0
|
@ -1 +0,0 @@
|
||||
allow_empty: false
|
@ -1 +0,0 @@
|
||||
foo
|
@ -1 +0,0 @@
|
||||
# comment
|
@ -1 +0,0 @@
|
||||
lorem modus
|
@ -1 +0,0 @@
|
||||
mode: 0755
|
@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
from __future__ import print_function
|
||||
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,430 +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 atexit
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import fixtures
|
||||
import mock
|
||||
import testtools
|
||||
|
||||
from os_apply_config import apply_config
|
||||
from os_apply_config import config_exception as exc
|
||||
from os_apply_config import oac_file
|
||||
|
||||
# example template tree
|
||||
TEMPLATES = os.path.join(os.path.dirname(__file__), 'templates')
|
||||
|
||||
# config for example tree
|
||||
CONFIG = {
|
||||
"x": "foo",
|
||||
"y": False,
|
||||
"z": None,
|
||||
"btrue": True,
|
||||
"bfalse": False,
|
||||
"database": {
|
||||
"url": "sqlite:///blah"
|
||||
},
|
||||
"l": [1, 2],
|
||||
}
|
||||
|
||||
# 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": oac_file.OacFile(
|
||||
"foo\n"),
|
||||
"/etc/keystone/keystone.conf": oac_file.OacFile(
|
||||
"[foo]\ndatabase = sqlite:///blah\n"),
|
||||
"/etc/control/empty": oac_file.OacFile(
|
||||
"foo\n"),
|
||||
"/etc/control/allow_empty": oac_file.OacFile(
|
||||
"").set('allow_empty', False),
|
||||
"/etc/control/mode": oac_file.OacFile(
|
||||
"lorem modus\n").set('mode', 0o755),
|
||||
}
|
||||
TEMPLATE_PATHS = OUTPUT.keys()
|
||||
|
||||
# expected output for chown tests
|
||||
# separated out to avoid needing to mock os.chown for most tests
|
||||
CHOWN_TEMPLATES = os.path.join(os.path.dirname(__file__), 'chown_templates')
|
||||
CHOWN_OUTPUT = {
|
||||
"owner.uid": oac_file.OacFile("lorem uido\n").set('owner', 0),
|
||||
"owner.name": oac_file.OacFile("namo uido\n").set('owner', 0),
|
||||
"group.gid": oac_file.OacFile("lorem gido\n").set('group', 0),
|
||||
"group.name": oac_file.OacFile("namo gido\n").set('group', 0),
|
||||
}
|
||||
|
||||
|
||||
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):
|
||||
"""Tests the commandline options."""
|
||||
|
||||
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, apply_config.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_json_dict(self):
|
||||
self.assertEqual(0, apply_config.main(
|
||||
['os-apply-config.py', '--metadata', self.path, '--key',
|
||||
'database', '--type', 'raw']))
|
||||
self.stdout.seek(0)
|
||||
self.assertEqual(CONFIG['database'],
|
||||
json.loads(self.stdout.read().strip()))
|
||||
self.assertEqual('', self.logger.output)
|
||||
|
||||
def test_print_key_json_list(self):
|
||||
self.assertEqual(0, apply_config.main(
|
||||
['os-apply-config.py', '--metadata', self.path, '--key',
|
||||
'l', '--type', 'raw']))
|
||||
self.stdout.seek(0)
|
||||
self.assertEqual(CONFIG['l'],
|
||||
json.loads(self.stdout.read().strip()))
|
||||
self.assertEqual('', self.logger.output)
|
||||
|
||||
def test_print_non_string_key(self):
|
||||
self.assertEqual(0, apply_config.main(
|
||||
['os-apply-config.py', '--metadata', self.path, '--key',
|
||||
'y', '--type', 'raw']))
|
||||
self.stdout.seek(0)
|
||||
self.assertEqual("false",
|
||||
self.stdout.read().strip())
|
||||
self.assertEqual('', self.logger.output)
|
||||
|
||||
def test_print_null_key(self):
|
||||
self.assertEqual(0, apply_config.main(
|
||||
['os-apply-config.py', '--metadata', self.path, '--key',
|
||||
'z', '--type', 'raw', '--key-default', '']))
|
||||
self.stdout.seek(0)
|
||||
self.assertEqual('', self.stdout.read().strip())
|
||||
self.assertEqual('', self.logger.output)
|
||||
|
||||
def test_print_key_missing(self):
|
||||
self.assertEqual(1, apply_config.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, apply_config.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, apply_config.main(
|
||||
['os-apply-config.py', '--metadata', self.path, '--key',
|
||||
'x', '--type', 'int']))
|
||||
self.assertIn('cannot interpret value', self.logger.output)
|
||||
|
||||
def test_print_key_from_list(self):
|
||||
self.assertEqual(0, apply_config.main(
|
||||
['os-apply-config.py', '--metadata', self.path, '--key',
|
||||
'l.0', '--type', 'int']))
|
||||
self.stdout.seek(0)
|
||||
self.assertEqual(str(CONFIG['l'][0]),
|
||||
self.stdout.read().strip())
|
||||
self.assertEqual('', self.logger.output)
|
||||
|
||||
def test_print_key_from_list_missing(self):
|
||||
self.assertEqual(1, apply_config.main(
|
||||
['os-apply-config.py', '--metadata', self.path, '--key',
|
||||
'l.2', '--type', 'int']))
|
||||
self.assertIn('does not exist', self.logger.output)
|
||||
|
||||
def test_print_key_from_list_missing_default(self):
|
||||
self.assertEqual(0, apply_config.main(
|
||||
['os-apply-config.py', '--metadata', self.path, '--key',
|
||||
'l.2', '--type', 'int', '--key-default', '']))
|
||||
self.stdout.seek(0)
|
||||
self.assertEqual('', self.stdout.read().strip())
|
||||
self.assertEqual('', self.logger.output)
|
||||
|
||||
def test_print_templates(self):
|
||||
apply_config.main(['os-apply-config', '--print-templates'])
|
||||
self.stdout.seek(0)
|
||||
self.assertEqual(
|
||||
self.stdout.read().strip(), apply_config.TEMPLATES_DIR)
|
||||
self.assertEqual('', self.logger.output)
|
||||
|
||||
def test_boolean_key(self):
|
||||
rcode = apply_config.main(['os-apply-config', '--metadata',
|
||||
self.path, '--boolean-key', 'btrue'])
|
||||
self.assertEqual(0, rcode)
|
||||
rcode = apply_config.main(['os-apply-config', '--metadata',
|
||||
self.path, '--boolean-key', 'bfalse'])
|
||||
self.assertEqual(1, rcode)
|
||||
rcode = apply_config.main(['os-apply-config', '--metadata',
|
||||
self.path, '--boolean-key', 'x'])
|
||||
self.assertEqual(-1, rcode)
|
||||
|
||||
def test_boolean_key_and_key(self):
|
||||
rcode = apply_config.main(['os-apply-config', '--metadata',
|
||||
self.path, '--boolean-key', 'btrue',
|
||||
'--key', 'x'])
|
||||
self.assertEqual(0, rcode)
|
||||
self.stdout.seek(0)
|
||||
self.assertEqual(self.stdout.read().strip(), 'foo')
|
||||
self.assertIn('--boolean-key ignored', self.logger.output)
|
||||
|
||||
def test_os_config_files(self):
|
||||
with tempfile.NamedTemporaryFile() as fake_os_config_files:
|
||||
with tempfile.NamedTemporaryFile() as fake_config:
|
||||
fake_config.write(json.dumps(CONFIG).encode('utf-8'))
|
||||
fake_config.flush()
|
||||
fake_os_config_files.write(
|
||||
json.dumps([fake_config.name]).encode('utf-8'))
|
||||
fake_os_config_files.flush()
|
||||
apply_config.main(['os-apply-config',
|
||||
'--key', 'database.url',
|
||||
'--type', 'raw',
|
||||
'--os-config-files',
|
||||
fake_os_config_files.name])
|
||||
self.stdout.seek(0)
|
||||
self.assertEqual(
|
||||
CONFIG['database']['url'], self.stdout.read().strip())
|
||||
|
||||
|
||||
class OSConfigApplierTestCase(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(OSConfigApplierTestCase, self).setUp()
|
||||
self.logger = self.useFixture(fixtures.FakeLogger('os-apply-config'))
|
||||
self.useFixture(fixtures.NestedTempfile())
|
||||
|
||||
def write_config(self, config):
|
||||
fd, path = tempfile.mkstemp()
|
||||
with os.fdopen(fd, 'w') as t:
|
||||
t.write(json.dumps(config))
|
||||
t.flush()
|
||||
return path
|
||||
|
||||
def check_output_file(self, tmpdir, path, obj):
|
||||
full_path = os.path.join(tmpdir, path[1:])
|
||||
if obj.allow_empty:
|
||||
assert os.path.exists(full_path), "%s doesn't exist" % path
|
||||
self.assertEqual(obj.body, open(full_path).read())
|
||||
else:
|
||||
assert not os.path.exists(full_path), "%s exists" % path
|
||||
|
||||
def test_install_config(self):
|
||||
path = self.write_config(CONFIG)
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
apply_config.install_config([path], TEMPLATES, tmpdir, False)
|
||||
for path, obj in OUTPUT.items():
|
||||
self.check_output_file(tmpdir, path, obj)
|
||||
|
||||
def test_install_config_subhash(self):
|
||||
tpath = self.write_config(CONFIG_SUBHASH)
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
apply_config.install_config(
|
||||
[tpath], TEMPLATES, tmpdir, False, 'OpenStack::Config')
|
||||
for path, obj in OUTPUT.items():
|
||||
self.check_output_file(tmpdir, path, obj)
|
||||
|
||||
def test_delete_if_not_allowed_empty(self):
|
||||
path = self.write_config(CONFIG)
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
template = "/etc/control/allow_empty"
|
||||
target_file = os.path.join(tmpdir, template[1:])
|
||||
# Touch the file
|
||||
os.makedirs(os.path.dirname(target_file))
|
||||
open(target_file, 'a').close()
|
||||
apply_config.install_config([path], TEMPLATES, tmpdir, False)
|
||||
# File should be gone
|
||||
self.assertFalse(os.path.exists(target_file))
|
||||
|
||||
def test_respect_file_permissions(self):
|
||||
path = self.write_config(CONFIG)
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
template = "/etc/keystone/keystone.conf"
|
||||
target_file = os.path.join(tmpdir, template[1:])
|
||||
os.makedirs(os.path.dirname(target_file))
|
||||
# File doesn't exist, use the default mode (644)
|
||||
apply_config.install_config([path], TEMPLATES, tmpdir, False)
|
||||
self.assertEqual(0o100644, os.stat(target_file).st_mode)
|
||||
self.assertEqual(OUTPUT[template].body, open(target_file).read())
|
||||
# Set a different mode:
|
||||
os.chmod(target_file, 0o600)
|
||||
apply_config.install_config([path], TEMPLATES, tmpdir, False)
|
||||
# The permissions should be preserved
|
||||
self.assertEqual(0o100600, os.stat(target_file).st_mode)
|
||||
self.assertEqual(OUTPUT[template].body, open(target_file).read())
|
||||
|
||||
def test_build_tree(self):
|
||||
tree = apply_config.build_tree(
|
||||
apply_config.template_paths(TEMPLATES), CONFIG)
|
||||
self.assertEqual(OUTPUT, tree)
|
||||
|
||||
def test_render_template(self):
|
||||
# execute executable files, moustache non-executables
|
||||
self.assertEqual("abc\n", apply_config.render_template(template(
|
||||
"/etc/glance/script.conf"), {"x": "abc"}))
|
||||
self.assertRaises(
|
||||
exc.ConfigException,
|
||||
apply_config.render_template,
|
||||
template("/etc/glance/script.conf"), {})
|
||||
|
||||
def test_render_template_bad_template(self):
|
||||
tdir = self.useFixture(fixtures.TempDir())
|
||||
bt_path = os.path.join(tdir.path, 'bad_template')
|
||||
with open(bt_path, 'w') as bt:
|
||||
bt.write("{{#foo}}bar={{bar}}{{/bar}}")
|
||||
e = self.assertRaises(exc.ConfigException,
|
||||
apply_config.render_template,
|
||||
bt_path, {'foo': [{'bar':
|
||||
'abc'}]})
|
||||
self.assertIn('could not render moustache template', str(e))
|
||||
self.assertIn('Section end tag mismatch', self.logger.output)
|
||||
|
||||
def test_render_moustache(self):
|
||||
self.assertEqual(
|
||||
"ab123cd",
|
||||
apply_config.render_moustache("ab{{x.a}}cd", {"x": {"a": "123"}}))
|
||||
|
||||
def test_render_moustache_bad_key(self):
|
||||
self.assertEqual(u'', apply_config.render_moustache("{{badkey}}", {}))
|
||||
|
||||
def test_render_executable(self):
|
||||
params = {"x": "foo"}
|
||||
self.assertEqual("foo\n", apply_config.render_executable(
|
||||
template("/etc/glance/script.conf"), params))
|
||||
|
||||
def test_render_executable_failure(self):
|
||||
self.assertRaises(
|
||||
exc.ConfigException,
|
||||
apply_config.render_executable,
|
||||
template("/etc/glance/script.conf"), {})
|
||||
|
||||
def test_template_paths(self):
|
||||
expected = list(map(lambda p: (template(p), p), TEMPLATE_PATHS))
|
||||
actual = apply_config.template_paths(TEMPLATES)
|
||||
expected.sort(key=lambda tup: tup[1])
|
||||
actual.sort(key=lambda tup: tup[1])
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_strip_hash(self):
|
||||
h = {'a': {'b': {'x': 'y'}}, "c": [1, 2, 3]}
|
||||
self.assertEqual({'x': 'y'}, apply_config.strip_hash(h, 'a.b'))
|
||||
self.assertRaises(exc.ConfigException,
|
||||
apply_config.strip_hash, h, 'a.nonexistent')
|
||||
self.assertRaises(exc.ConfigException,
|
||||
apply_config.strip_hash, h, 'a.c')
|
||||
|
||||
def test_load_list_from_json(self):
|
||||
def mkstemp():
|
||||
fd, path = tempfile.mkstemp()
|
||||
atexit.register(
|
||||
lambda: os.path.exists(path) and os.remove(path))
|
||||
return (fd, path)
|
||||
|
||||
def write_contents(fd, contents):
|
||||
with os.fdopen(fd, 'w') as t:
|
||||
t.write(contents)
|
||||
t.flush()
|
||||
|
||||
fd, path = mkstemp()
|
||||
load_list = apply_config.load_list_from_json
|
||||
self.assertRaises(ValueError, load_list, path)
|
||||
write_contents(fd, json.dumps(["/tmp/config.json"]))
|
||||
json_obj = load_list(path)
|
||||
self.assertEqual(["/tmp/config.json"], json_obj)
|
||||
os.remove(path)
|
||||
self.assertEqual([], load_list(path))
|
||||
|
||||
fd, path = mkstemp()
|
||||
write_contents(fd, json.dumps({}))
|
||||
self.assertRaises(ValueError, load_list, path)
|
||||
|
||||
def test_default_templates_dir_current(self):
|
||||
default = '/usr/libexec/os-apply-config/templates'
|
||||
with mock.patch('os.path.isdir', lambda x: x == default):
|
||||
self.assertEqual(default, apply_config.templates_dir())
|
||||
|
||||
def test_default_templates_dir_deprecated(self):
|
||||
default = '/opt/stack/os-apply-config/templates'
|
||||
with mock.patch('os.path.isdir', lambda x: x == default):
|
||||
self.assertEqual(default, apply_config.templates_dir())
|
||||
|
||||
def test_default_templates_dir_old_deprecated(self):
|
||||
default = '/opt/stack/os-config-applier/templates'
|
||||
with mock.patch('os.path.isdir', lambda x: x == default):
|
||||
self.assertEqual(default, apply_config.templates_dir())
|
||||
|
||||
def test_default_templates_dir_both(self):
|
||||
default = '/usr/libexec/os-apply-config/templates'
|
||||
deprecated = '/opt/stack/os-apply-config/templates'
|
||||
with mock.patch('os.path.isdir', lambda x: (x == default or
|
||||
x == deprecated)):
|
||||
self.assertEqual(default, apply_config.templates_dir())
|
||||
|
||||
def test_control_mode(self):
|
||||
path = self.write_config(CONFIG)
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
template = "/etc/control/mode"
|
||||
target_file = os.path.join(tmpdir, template[1:])
|
||||
apply_config.install_config([path], TEMPLATES, tmpdir, False)
|
||||
self.assertEqual(0o100755, os.stat(target_file).st_mode)
|
||||
|
||||
@mock.patch('os.chown')
|
||||
def test_control_chown(self, chown_mock):
|
||||
path = self.write_config(CONFIG)
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
apply_config.install_config([path], CHOWN_TEMPLATES, tmpdir, False)
|
||||
chown_mock.assert_has_calls([mock.call(mock.ANY, 0, -1), # uid
|
||||
mock.call(mock.ANY, 0, -1), # username
|
||||
mock.call(mock.ANY, -1, 0), # gid
|
||||
mock.call(mock.ANY, -1, 0)], # groupname
|
||||
any_order=True)
|
@ -1,121 +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 fixtures
|
||||
import testtools
|
||||
|
||||
from os_apply_config import collect_config
|
||||
from os_apply_config import config_exception as exc
|
||||
|
||||
|
||||
class OCCTestCase(testtools.TestCase):
|
||||
def test_collect_config(self):
|
||||
conflict_configs = [('ec2', {'local-ipv4': '192.0.2.99',
|
||||
'instance-id': 'feeddead'}),
|
||||
('cfn', {'foo': {'bar': 'foo-bar'},
|
||||
'local-ipv4': '198.51.100.50'})]
|
||||
config_files = []
|
||||
tdir = self.useFixture(fixtures.TempDir())
|
||||
for name, config in conflict_configs:
|
||||
path = os.path.join(tdir.path, '%s.json' % name)
|
||||
with open(path, 'w') as out:
|
||||
out.write(json.dumps(config))
|
||||
config_files.append(path)
|
||||
config = collect_config.collect_config(config_files)
|
||||
self.assertEqual(
|
||||
{'local-ipv4': '198.51.100.50',
|
||||
'instance-id': 'feeddead',
|
||||
'foo': {'bar': 'foo-bar'}}, config)
|
||||
|
||||
def test_collect_config_fallback(self):
|
||||
tdir = self.useFixture(fixtures.TempDir())
|
||||
with open(os.path.join(tdir.path, 'does_exist.json'), 'w') as t:
|
||||
t.write(json.dumps({'a': 1}))
|
||||
noexist_path = os.path.join(tdir.path, 'does_not_exist.json')
|
||||
|
||||
config = collect_config.collect_config([], [noexist_path, t.name])
|
||||
self.assertEqual({'a': 1}, config)
|
||||
|
||||
with open(os.path.join(tdir.path, 'does_exist_new.json'), 'w') as t2:
|
||||
t2.write(json.dumps({'a': 2}))
|
||||
|
||||
config = collect_config.collect_config([t2.name], [t.name])
|
||||
self.assertEqual({'a': 2}, config)
|
||||
|
||||
config = collect_config.collect_config([], [t.name, noexist_path])
|
||||
self.assertEqual({'a': 1}, config)
|
||||
self.assertEqual({},
|
||||
collect_config.collect_config([], [noexist_path]))
|
||||
self.assertEqual({},
|
||||
collect_config.collect_config([]))
|
||||
|
||||
def test_failed_read(self):
|
||||
tdir = self.useFixture(fixtures.TempDir())
|
||||
unreadable_path = os.path.join(tdir.path, 'unreadable.json')
|
||||
with open(unreadable_path, 'w') as u:
|
||||
u.write(json.dumps({}))
|
||||
os.chmod(unreadable_path, 0o000)
|
||||
self.assertRaises(
|
||||
exc.ConfigException,
|
||||
lambda: list(collect_config.read_configs([unreadable_path])))
|
||||
|
||||
def test_bad_json(self):
|
||||
tdir = self.useFixture(fixtures.TempDir())
|
||||
bad_json_path = os.path.join(tdir.path, 'bad.json')
|
||||
self.assertRaises(
|
||||
exc.ConfigException,
|
||||
lambda: list(collect_config.parse_configs([('{', bad_json_path)])))
|
||||
|
||||
|
||||
class TestMergeConfigs(testtools.TestCase):
|
||||
|
||||
def test_merge_configs_noconflict(self):
|
||||
noconflict_configs = [{'a': '1'},
|
||||
{'b': 'Y'}]
|
||||
result = collect_config.merge_configs(noconflict_configs)
|
||||
self.assertEqual({'a': '1',
|
||||
'b': 'Y'}, result)
|
||||
|
||||
def test_merge_configs_conflict(self):
|
||||
conflict_configs = [{'a': '1'}, {'a': 'Z'}]
|
||||
result = collect_config.merge_configs(conflict_configs)
|
||||
self.assertEqual({'a': 'Z'}, result)
|
||||
|
||||
def test_merge_configs_deep_conflict(self):
|
||||
deepconflict_conf = [{'a': '1'},
|
||||
{'b': {'x': 'foo-bar', 'y': 'tribbles'}},
|
||||
{'b': {'x': 'shazam'}}]
|
||||
result = collect_config.merge_configs(deepconflict_conf)
|
||||
self.assertEqual({'a': '1',
|
||||
'b': {'x': 'shazam', 'y': 'tribbles'}}, result)
|
||||
|
||||
def test_merge_configs_type_conflict(self):
|
||||
type_conflict = [{'a': 1}, {'a': [7, 8, 9]}]
|
||||
result = collect_config.merge_configs(type_conflict)
|
||||
self.assertEqual({'a': [7, 8, 9]}, result)
|
||||
|
||||
def test_merge_configs_list_conflict(self):
|
||||
list_conflict = [{'a': [1, 2, 3]},
|
||||
{'a': [4, 5, 6]}]
|
||||
result = collect_config.merge_configs(list_conflict)
|
||||
self.assertEqual({'a': [4, 5, 6]}, result)
|
||||
|
||||
def test_merge_configs_empty_notdict(self):
|
||||
list_conflict = [[], {'a': '1'}, '', None, {'b': '2'}, {}]
|
||||
result = collect_config.merge_configs(list_conflict)
|
||||
self.assertEqual({'a': '1', 'b': '2'}, result)
|
@ -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,90 +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 grp
|
||||
import pwd
|
||||
|
||||
import testtools
|
||||
|
||||
from os_apply_config import config_exception as exc
|
||||
from os_apply_config import oac_file
|
||||
|
||||
|
||||
class OacFileTestCase(testtools.TestCase):
|
||||
def test_mode_string(self):
|
||||
oacf = oac_file.OacFile('')
|
||||
mode = '0644'
|
||||
try:
|
||||
oacf.mode = mode
|
||||
except exc.ConfigException as e:
|
||||
self.assertIn("mode '%s' is not numeric" % mode, str(e))
|
||||
|
||||
def test_mode_range(self):
|
||||
oacf = oac_file.OacFile('')
|
||||
for mode in [-1, 0o1000]:
|
||||
try:
|
||||
oacf.mode = mode
|
||||
except exc.ConfigException as e:
|
||||
self.assertTrue("mode '%#o' out of range" % mode in str(e),
|
||||
"mode: %#o" % mode)
|
||||
|
||||
for mode in [0, 0o777]:
|
||||
oacf.mode = mode
|
||||
|
||||
def test_owner_positive(self):
|
||||
oacf = oac_file.OacFile('')
|
||||
users = pwd.getpwall()
|
||||
for name in [user[0] for user in users]:
|
||||
oacf.owner = name
|
||||
for uid in [user[2] for user in users]:
|
||||
oacf.owner = uid
|
||||
|
||||
def test_owner_negative(self):
|
||||
oacf = oac_file.OacFile('')
|
||||
try:
|
||||
user = -1
|
||||
oacf.owner = user
|
||||
except exc.ConfigException as e:
|
||||
self.assertIn(
|
||||
"owner '%s' not found in passwd database" % user, str(e))
|
||||
try:
|
||||
user = "za"
|
||||
oacf.owner = user
|
||||
except exc.ConfigException as e:
|
||||
self.assertIn(
|
||||
"owner '%s' not found in passwd database" % user, str(e))
|
||||
|
||||
def test_group_positive(self):
|
||||
oacf = oac_file.OacFile('')
|
||||
groups = grp.getgrall()
|
||||
for name in [group[0] for group in groups]:
|
||||
oacf.group = name
|
||||
for gid in [group[2] for group in groups]:
|
||||
oacf.group = gid
|
||||
|
||||
def test_group_negative(self):
|
||||
oacf = oac_file.OacFile('')
|
||||
try:
|
||||
group = -1
|
||||
oacf.group = group
|
||||
except exc.ConfigException as e:
|
||||
self.assertIn(
|
||||
"group '%s' not found in group database" % group, str(e))
|
||||
try:
|
||||
group = "za"
|
||||
oacf.group = group
|
||||
except exc.ConfigException as e:
|
||||
self.assertIn(
|
||||
"group '%s' not found in group database" % group, str(e))
|
@ -1,158 +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_empty(self):
|
||||
self.assertEqual('', value_types.ensure_type('', 'netaddress'))
|
||||
|
||||
def test_net_address_bad(self):
|
||||
self.assertRaises(config_exception.ConfigException,
|
||||
value_types.ensure_type, "192.0.2.1;DROP TABLE foo",
|
||||
'netaddress')
|
||||
|
||||
def test_netdevice(self):
|
||||
self.assertEqual('eth0',
|
||||
value_types.ensure_type('eth0', 'netdevice'))
|
||||
|
||||
def test_netdevice_dash(self):
|
||||
self.assertEqual('br-ctlplane',
|
||||
value_types.ensure_type('br-ctlplane', 'netdevice'))
|
||||
|
||||
def test_netdevice_alias(self):
|
||||
self.assertEqual('eth0:1',
|
||||
value_types.ensure_type('eth0:1', 'netdevice'))
|
||||
|
||||
def test_netdevice_bad(self):
|
||||
self.assertRaises(config_exception.ConfigException,
|
||||
value_types.ensure_type, "br-tun; DROP TABLE bar",
|
||||
'netdevice')
|
||||
|
||||
def test_dsn_nopass(self):
|
||||
test_dsn = 'mysql://user@host/db'
|
||||
self.assertEqual(test_dsn, value_types.ensure_type(test_dsn, 'dsn'))
|
||||
|
||||
def test_dsn(self):
|
||||
test_dsn = 'mysql://user:pass@host/db'
|
||||
self.assertEqual(test_dsn, value_types.ensure_type(test_dsn, 'dsn'))
|
||||
|
||||
def test_dsn_set_variables(self):
|
||||
test_dsn = 'mysql://user:pass@host/db?charset=utf8'
|
||||
self.assertEqual(test_dsn, value_types.ensure_type(test_dsn, 'dsn'))
|
||||
|
||||
def test_dsn_sqlite_memory(self):
|
||||
test_dsn = 'sqlite://'
|
||||
self.assertEqual(test_dsn, value_types.ensure_type(test_dsn, 'dsn'))
|
||||
|
||||
def test_dsn_sqlite_file(self):
|
||||
test_dsn = 'sqlite:///tmp/foo.db'
|
||||
self.assertEqual(test_dsn, value_types.ensure_type(test_dsn, 'dsn'))
|
||||
|
||||
def test_dsn_bad(self):
|
||||
self.assertRaises(config_exception.ConfigException,
|
||||
value_types.ensure_type,
|
||||
"mysql:/user:pass@host/db?charset=utf8", 'dsn')
|
||||
self.assertRaises(config_exception.ConfigException,
|
||||
value_types.ensure_type,
|
||||
"mysql://user:pass@host/db?charset=utf8;DROP TABLE "
|
||||
"foo", 'dsn')
|
||||
|
||||
def test_swiftdevices_single(self):
|
||||
test_swiftdevices = 'r1z1-127.0.0.1:%PORT%/d1'
|
||||
self.assertEqual(test_swiftdevices, value_types.ensure_type(
|
||||
test_swiftdevices,
|
||||
'swiftdevices'))
|
||||
|
||||
def test_swiftdevices_multi(self):
|
||||
test_swiftdevices = 'r1z1-127.0.0.1:%PORT%/d1,r1z1-127.0.0.1:%PORT%/d2'
|
||||
self.assertEqual(test_swiftdevices, value_types.ensure_type(
|
||||
test_swiftdevices,
|
||||
'swiftdevices'))
|
||||
|
||||
def test_swiftdevices_blank(self):
|
||||
test_swiftdevices = ''
|
||||
self.assertRaises(config_exception.ConfigException,
|
||||
value_types.ensure_type,
|
||||
test_swiftdevices,
|
||||
'swiftdevices')
|
||||
|
||||
def test_swiftdevices_bad(self):
|
||||
test_swiftdevices = 'rz1-127.0.0.1:%PORT%/d1'
|
||||
self.assertRaises(config_exception.ConfigException,
|
||||
value_types.ensure_type,
|
||||
test_swiftdevices,
|
||||
'swiftdevices')
|
||||
|
||||
def test_username(self):
|
||||
for test_username in ['guest', 'guest_13-42']:
|
||||
self.assertEqual(test_username, value_types.ensure_type(
|
||||
test_username,
|
||||
'username'))
|
||||
|
||||
def test_username_bad(self):
|
||||
for test_username in ['guest`ls`', 'guest$PASSWD', 'guest 2']:
|
||||
self.assertRaises(config_exception.ConfigException,
|
||||
value_types.ensure_type,
|
||||
test_username,
|
||||
'username')
|
@ -1,44 +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 os_apply_config import config_exception
|
||||
|
||||
TYPES = {
|
||||
"int": "^[0-9]+$",
|
||||
"default": "^[A-Za-z0-9_]*$",
|
||||
"netaddress": "^[A-Za-z0-9/.:-]*$",
|
||||
"netdevice": "^[A-Za-z0-9/.:-]*$",
|
||||
"dsn": "(?#driver)^[a-zA-Z0-9]+://"
|
||||
"(?#username[:password])([a-zA-Z0-9+_-]+(:[^@]+)?)?"
|
||||
"(?#@host or file)(@?[a-zA-Z0-9/_.-]+)?"
|
||||
"(?#/dbname)(/[a-zA-Z0-9_-]+)?"
|
||||
"(?#?variable=value)(\?[a-zA-Z0-9=_-]+)?$",
|
||||
"swiftdevices": "^(r\d+z\d+-[A-Za-z0-9.-_]+:%PORT%/[^,]+,?)+$",
|
||||
"username": "^[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):
|
||||
exception = config_exception.ConfigException
|
||||
raise exception("cannot interpret value '%s' as type %s" % (
|
||||
string_value, type_name))
|
||||
return string_value
|
@ -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.
|
||||
|
||||
|
||||
import pbr.version
|
||||
|
||||
version_info = pbr.version.VersionInfo('os-apply-config')
|
@ -1,9 +0,0 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
||||
|
||||
anyjson>=0.3.3 # BSD
|
||||
pystache # MIT
|
||||
PyYAML>=3.10.0 # MIT
|
||||
six>=1.9.0 # MIT
|
31
setup.cfg
31
setup.cfg
@ -1,31 +0,0 @@
|
||||
[metadata]
|
||||
name = os-apply-config
|
||||
author = OpenStack
|
||||
author-email = openstack-dev@lists.openstack.org
|
||||
summary = Config files from cloud metadata
|
||||
description-file =
|
||||
README.rst
|
||||
home-page = http://git.openstack.org/cgit/openstack/os-apply-config
|
||||
classifier =
|
||||
Development Status :: 4 - Beta
|
||||
Environment :: Console
|
||||
Environment :: OpenStack
|
||||
Intended Audience :: Developers
|
||||
Intended Audience :: Information Technology
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: OS Independent
|
||||
Programming Language :: Python
|
||||
|
||||
[files]
|
||||
packages =
|
||||
os_apply_config
|
||||
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
os-config-applier = os_apply_config.apply_config:main
|
||||
os-apply-config = os_apply_config.apply_config:main
|
||||
|
||||
[egg_info]
|
||||
tag_build =
|
||||
tag_date = 0
|
||||
tag_svn_revision = 0
|
29
setup.py
29
setup.py
@ -1,29 +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.
|
||||
|
||||
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
|
||||
import setuptools
|
||||
|
||||
# In python < 2.7.4, a lazy loading of package `pbr` will break
|
||||
# setuptools if some other modules registered functions in `atexit`.
|
||||
# solution from: http://bugs.python.org/issue15881#msg170215
|
||||
try:
|
||||
import multiprocessing # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['pbr>=2.0.0'],
|
||||
pbr=True)
|
@ -1,13 +0,0 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
|
||||
|
||||
coverage!=4.4,>=4.0 # Apache-2.0
|
||||
fixtures>=3.0.0 # Apache-2.0/BSD
|
||||
mock>=2.0 # BSD
|
||||
python-subunit>=0.0.18 # Apache-2.0/BSD
|
||||
sphinx>=1.6.2 # BSD
|
||||
testrepository>=0.0.18 # Apache-2.0/BSD
|
||||
testscenarios>=0.4 # Apache-2.0/BSD
|
||||
testtools>=1.4.0 # MIT
|
@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Client constraint file contains this client version pin that is in conflict
|
||||
# with installing the client from source. We should remove the version pin in
|
||||
# the constraints file before applying it for from-source installation.
|
||||
|
||||
CONSTRAINTS_FILE="$1"
|
||||
shift 1
|
||||
|
||||
set -e
|
||||
|
||||
# NOTE(tonyb): Place this in the tox enviroment's log dir so it will get
|
||||
# published to logs.openstack.org for easy debugging.
|
||||
localfile="$VIRTUAL_ENV/log/upper-constraints.txt"
|
||||
|
||||
if [[ "$CONSTRAINTS_FILE" != http* ]]; then
|
||||
CONSTRAINTS_FILE="file://$CONSTRAINTS_FILE"
|
||||
fi
|
||||
# NOTE(tonyb): need to add curl to bindep.txt if the project supports bindep
|
||||
curl "$CONSTRAINTS_FILE" --insecure --progress-bar --output "$localfile"
|
||||
|
||||
pip install -c"$localfile" openstack-requirements
|
||||
|
||||
# This is the main purpose of the script: Allow local installation of
|
||||
# the current repo. It is listed in constraints file and thus any
|
||||
# install will be constrained and we need to unconstrain it.
|
||||
edit-constraints "$localfile" -- "$CLIENT_NAME"
|
||||
|
||||
pip install -c"$localfile" -U "$@"
|
||||
exit $?
|
32
tox.ini
32
tox.ini
@ -1,32 +0,0 @@
|
||||
[tox]
|
||||
minversion = 2.0
|
||||
skipsdist = True
|
||||
envlist = py27,pep8
|
||||
|
||||
[testenv]
|
||||
usedevelop = True
|
||||
install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
|
||||
setenv = VIRTUAL_ENV={envdir}
|
||||
BRANCH_NAME=master
|
||||
CLIENT_NAME=os-apply-config
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
commands =
|
||||
python setup.py testr --slowest --testr-args='{posargs}'
|
||||
|
||||
[tox:jenkins]
|
||||
sitepackages = True
|
||||
|
||||
[testenv:pep8]
|
||||
commands = flake8
|
||||
|
||||
[testenv:cover]
|
||||
commands =
|
||||
python setup.py test --coverage --coverage-package-name=os_apply_config
|
||||
|
||||
[testenv:venv]
|
||||
commands = {posargs}
|
||||
|
||||
[flake8]
|
||||
exclude = .venv,.tox,dist,doc,*.egg
|
||||
show-source = true
|
Loading…
Reference in New Issue
Block a user