Add helpers for cdist config/install integration. (#551)

Implement simple integration API.
This commit is contained in:
Darko Poljak 2017-09-01 14:08:50 +02:00 committed by GitHub
parent feb221c5df
commit 136f2ecd87
8 changed files with 238 additions and 13 deletions

View File

@ -73,8 +73,6 @@ def check_beta(args_dict):
def check_positive_int(value): def check_positive_int(value):
import argparse
try: try:
val = int(value) val = int(value)
except ValueError: except ValueError:
@ -416,10 +414,10 @@ def handle_loglevel(args):
logging.root.setLevel(_verbosity_level[args.verbose]) logging.root.setLevel(_verbosity_level[args.verbose])
def parse_and_configure(argv): def parse_and_configure(argv, singleton=True):
parser = get_parsers() parser = get_parsers()
parser_args = parser['main'].parse_args(argv) parser_args = parser['main'].parse_args(argv)
cfg = cdist.configuration.Configuration(parser_args) cfg = cdist.configuration.Configuration(parser_args, singleton=singleton)
args = cfg.get_args() args = cfg.get_args()
# Loglevels are handled globally in here # Loglevels are handled globally in here
handle_loglevel(args) handle_loglevel(args)

View File

@ -45,7 +45,7 @@ class Config(object):
"""Cdist main class to hold arbitrary data""" """Cdist main class to hold arbitrary data"""
def __init__(self, local, remote, dry_run=False, jobs=None, def __init__(self, local, remote, dry_run=False, jobs=None,
cleanup_cmds=None): cleanup_cmds=None, remove_remote_files_dirs=False):
self.local = local self.local = local
self.remote = remote self.remote = remote
@ -56,6 +56,7 @@ class Config(object):
self.cleanup_cmds = cleanup_cmds self.cleanup_cmds = cleanup_cmds
else: else:
self.cleanup_cmds = [] self.cleanup_cmds = []
self.remove_remote_files_dirs = remove_remote_files_dirs
self.explorer = core.Explorer(self.local.target_host, self.local, self.explorer = core.Explorer(self.local.target_host, self.local,
self.remote, jobs=self.jobs) self.remote, jobs=self.jobs)
@ -67,6 +68,15 @@ class Config(object):
self.local.create_files_dirs() self.local.create_files_dirs()
self.remote.create_files_dirs() self.remote.create_files_dirs()
def _remove_remote_files_dirs(self):
"""Remove remote files and directories for the run"""
self.remote.remove_files_dirs()
def _remove_files_dirs(self):
"""Remove files and directories for the run"""
if self.remove_remote_files_dirs:
self._remove_remote_files_dirs()
@staticmethod @staticmethod
def hosts(source): def hosts(source):
try: try:
@ -283,7 +293,7 @@ class Config(object):
@classmethod @classmethod
def onehost(cls, host, host_tags, host_base_path, host_dir_name, args, def onehost(cls, host, host_tags, host_base_path, host_dir_name, args,
parallel, configuration): parallel, configuration, remove_remote_files_dirs=False):
"""Configure ONE system. """Configure ONE system.
If operating in parallel then return tuple (host, True|False, ) If operating in parallel then return tuple (host, True|False, )
so that main process knows for which host function was successful. so that main process knows for which host function was successful.
@ -311,7 +321,8 @@ class Config(object):
add_conf_dirs=args.conf_dir, add_conf_dirs=args.conf_dir,
cache_path_pattern=args.cache_path_pattern, cache_path_pattern=args.cache_path_pattern,
quiet_mode=args.quiet, quiet_mode=args.quiet,
configuration=configuration) configuration=configuration,
exec_path=sys.argv[0])
remote = cdist.exec.remote.Remote( remote = cdist.exec.remote.Remote(
target_host=target_host, target_host=target_host,
@ -326,7 +337,8 @@ class Config(object):
if cleanup_cmd: if cleanup_cmd:
cleanup_cmds.append(cleanup_cmd) cleanup_cmds.append(cleanup_cmd)
c = cls(local, remote, dry_run=args.dry_run, jobs=args.jobs, c = cls(local, remote, dry_run=args.dry_run, jobs=args.jobs,
cleanup_cmds=cleanup_cmds) cleanup_cmds=cleanup_cmds,
remove_remote_files_dirs=remove_remote_files_dirs)
c.run() c.run()
except cdist.Error as e: except cdist.Error as e:
@ -367,6 +379,7 @@ class Config(object):
self.manifest.run_initial_manifest(self.local.initial_manifest) self.manifest.run_initial_manifest(self.local.initial_manifest)
self.iterate_until_finished() self.iterate_until_finished()
self.cleanup() self.cleanup()
self._remove_files_dirs()
self.local.save_cache(start_time) self.local.save_cache(start_time)
self.log.info("Finished successful run in {:.2f} seconds".format( self.log.info("Finished successful run in {:.2f} seconds".format(

View File

@ -32,9 +32,12 @@ class Singleton(type):
instance = None instance = None
def __call__(cls, *args, **kwargs): def __call__(cls, *args, **kwargs):
if not cls.instance: if 'singleton' in kwargs and kwargs['singleton'] == False:
cls.instance = super(Singleton, cls).__call__(*args, **kwargs) return super(Singleton, cls).__call__(*args, **kwargs)
return cls.instance else:
if not cls.instance:
cls.instance = super(Singleton, cls).__call__(*args, **kwargs)
return cls.instance
_VERBOSITY_VALUES = ( _VERBOSITY_VALUES = (
@ -294,7 +297,7 @@ class Configuration(metaclass=Singleton):
return None return None
def __init__(self, command_line_args, env=os.environ, def __init__(self, command_line_args, env=os.environ,
config_files=default_config_files): config_files=default_config_files, singleton=True):
self.command_line_args = command_line_args self.command_line_args = command_line_args
self.args = self._convert_args(command_line_args) self.args = self._convert_args(command_line_args)
self.env = env self.env = env

View File

@ -116,6 +116,9 @@ class Remote(object):
self.run(["chmod", "0700", self.base_path]) self.run(["chmod", "0700", self.base_path])
self.mkdir(self.conf_path) self.mkdir(self.conf_path)
def remove_files_dirs(self):
self.rmdir(self.base_path)
def rmfile(self, path): def rmfile(self, path):
"""Remove file on the remote side.""" """Remove file on the remote side."""
self.log.trace("Remote rm: %s", path) self.log.trace("Remote rm: %s", path)

153
cdist/integration.py Normal file
View File

@ -0,0 +1,153 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# 2017 Darko Poljak (darko.poljak at gmail.com)
#
# This file is part of cdist.
#
# cdist is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# cdist is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with cdist. If not, see <http://www.gnu.org/licenses/>.
#
#
import cdist
# needed for cdist.argparse
import cdist.banner
import cdist.config
import cdist.install
import cdist.shell
import cdist.inventory
import cdist.argparse
import cdist.log
import cdist.config
import cdist.install
import sys
import os
import os.path
import collections
import uuid
def find_cdist_exec_in_path():
"""Search cdist executable in os.get_exec_path() entries.
"""
for path in os.get_exec_path():
cdist_path = os.path.join(path, 'cdist')
if os.access(cdist_path, os.X_OK):
return cdist_path
return None
_mydir = os.path.dirname(__file__)
def find_cdist_exec():
"""Search cdist executable starting from local lib directory.
Detect if ../scripts/cdist (from local lib direcotry) exists and
if it is executable. If not then try to find cdist exec path in
os.get_exec_path() entries. If no cdist path is found rasie
cdist.Error.
"""
cdist_path = os.path.abspath(os.path.join(_mydir, '..', 'scripts',
'cdist'))
if os.access(cdist_path, os.X_OK):
return cdist_path
cdist_path = find_cdist_exec_in_path()
if not cdist_path:
raise cdist.Error('Cannot find cdist executable from local lib '
'directory: {}, nor in PATH: {}.'.format(
_mydir, os.environ.get('PATH')))
return cdist_path
ACTION_CONFIG = 'config'
ACTION_INSTALL = 'install'
def _process_hosts_simple(action, host, manifest, verbose,
cdist_path=None):
"""Perform cdist action ('config' or 'install') on hosts with specified
manifest using default other cdist options. host parameter can be a
string or iterbale of hosts. verbose is a desired verbosity level
which defaults to VERBOSE_INFO. cdist_path is path to cdist executable,
if it is None then integration lib tries to find it.
"""
if isinstance(host, str):
hosts = [host, ]
elif isinstance(host, collections.Iterable):
hosts = host
else:
raise cdist.Error('Invalid host argument: {}'.format(host))
# Setup sys.argv[0] since cdist relies on command line invocation.
if not cdist_path:
cdist_path = find_cdist_exec()
sys.argv[0] = cdist_path
cname = action.title()
module = getattr(cdist, action)
theclass = getattr(module, cname)
# Build argv for cdist and use argparse for argument parsing.
remote_out_dir_base = os.path.join('/', 'var', 'lib', 'cdist')
uid = str(uuid.uuid1())
out_dir = remote_out_dir_base + uid
cache_path_pattern = '%h-' + uid
argv = [action, '-i', manifest, '-r', out_dir, '-C', cache_path_pattern, ]
for i in range(verbose):
argv.append('-v')
for x in hosts:
argv.append(x)
parser, cfg = cdist.argparse.parse_and_configure(argv, singleton=False)
args = cfg.get_args()
configuration = cfg.get_config(section='GLOBAL')
theclass.construct_remote_exec_copy_patterns(args)
base_root_path = theclass.create_base_root_path(None)
for target_host in args.host:
host_base_path, hostdir = theclass.create_host_base_dirs(
target_host, base_root_path)
theclass.onehost(target_host, None, host_base_path, hostdir, args,
parallel=False, configuration=configuration,
remove_remote_files_dirs=True)
def configure_hosts_simple(host, manifest,
verbose=cdist.argparse.VERBOSE_INFO,
cdist_path=None):
"""Configure hosts with specified manifest using default other cdist
options. host parameter can be a string or iterbale of hosts. verbose
is a desired verbosity level which defaults to VERBOSE_INFO.
cdist_path is path to cdist executable, if it is None then integration
lib tries to find it.
"""
_process_hosts_simple(action=ACTION_CONFIG, host=host,
manifest=manifest, verbose=verbose,
cdist_path=cdist_path)
def install_hosts_simple(host, manifest,
verbose=cdist.argparse.VERBOSE_INFO,
cdist_path=None):
"""Install hosts with specified manifest using default other cdist
options. host parameter can be a string or iterbale of hosts. verbose
is a desired verbosity level which defaults to VERBOSE_INFO.
cdist_path is path to cdist executable, if it is None then integration
lib tries to find it.
"""
_process_hosts_simple(action=ACTION_INSTALL, host=host,
manifest=manifest, verbose=verbose,
cdist_path=cdist_path)

View File

@ -236,9 +236,16 @@ class ConfigurationTestCase(test.CdistTestCase):
x = cc.Configuration(None) x = cc.Configuration(None)
args = argparse.Namespace() args = argparse.Namespace()
args.a = 'a' args.a = 'a'
y = cc.Configuration() y = cc.Configuration(args)
self.assertIs(x, y) self.assertIs(x, y)
def test_non_singleton(self):
x = cc.Configuration(None, singleton=False)
args = argparse.Namespace()
args.a = 'a'
y = cc.Configuration(args, singleton=False)
self.assertIsNot(x, y)
def test_read_config_file(self): def test_read_config_file(self):
config = cc.Configuration(None, env={}, config_files=()) config = cc.Configuration(None, env={}, config_files=())
d = config._read_config_file(self.config_file) d = config._read_config_file(self.config_file)

View File

@ -0,0 +1,47 @@
cdist integration / using cdist as library
==========================================
Description
-----------
cdist can be integrate with other applications by importing cdist and other
cdist modules and setting all by hand. There are also helper functions which
aim to ease this integration. Just import **cdist.integration** and use its
functions:
* :strong:`cdist.integration.configure_hosts_simple` for configuration
* :strong:`cdist.integration.install_hosts_simple` for installation.
Functions require `host` and `manifest` parameters.
`host` can be specified as a string representing host or as iterable
of hosts. `manifest` is a path to initial manifest. For other cdist
options default values will be used. `verbose` is a desired verbosity
level which defaults to VERBOSE_INFO. `cdist_path` parameter specifies
path to cdist executable, if it is `None` then functions will try to
find it first from local lib directory and then in PATH.
In case of cdist error :strong:`cdist.Error` exception is raised.
:strong:`WARNING`: cdist integration helper functions are not yet stable!
Examples
--------
.. code-block:: sh
# configure host from python interactive shell
>>> import cdist.integration
>>> cdist.integration.configure_hosts_simple('185.203.114.185',
... '~/.cdist/manifest/init')
# configure host from python interactive shell, specifying verbosity level
>>> import cdist.integration
>>> cdist.integration.configure_hosts_simple(
... '185.203.114.185', '~/.cdist/manifest/init',
... verbose=cdist.argparse.VERBOSE_TRACE)
# configure specified dns hosts from python interactive shell
>>> import cdist.integration
>>> hosts = ('dns1.ungleich.ch', 'dns2.ungleich.ch', 'dns3.ungleich.ch', )
>>> cdist.integration.configure_hosts_simple(hosts,
... '~/.cdist/manifest/init')

View File

@ -26,6 +26,7 @@ Contents:
cdist-messaging cdist-messaging
cdist-parallelization cdist-parallelization
cdist-inventory cdist-inventory
cdist-integration
cdist-reference cdist-reference
cdist-best-practice cdist-best-practice
cdist-stages cdist-stages