forked from ungleich-public/cdist
		
	Add helpers for cdist config/install integration. (#551)
Implement simple integration API.
This commit is contained in:
		
					parent
					
						
							
								feb221c5df
							
						
					
				
			
			
				commit
				
					
						136f2ecd87
					
				
			
		
					 8 changed files with 238 additions and 13 deletions
				
			
		|  | @ -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) | ||||||
|  |  | ||||||
|  | @ -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( | ||||||
|  |  | ||||||
|  | @ -32,6 +32,9 @@ class Singleton(type): | ||||||
|     instance = None |     instance = None | ||||||
| 
 | 
 | ||||||
|     def __call__(cls, *args, **kwargs): |     def __call__(cls, *args, **kwargs): | ||||||
|  |         if 'singleton' in kwargs and kwargs['singleton'] == False: | ||||||
|  |             return super(Singleton, cls).__call__(*args, **kwargs) | ||||||
|  |         else: | ||||||
|             if not cls.instance: |             if not cls.instance: | ||||||
|                 cls.instance = super(Singleton, cls).__call__(*args, **kwargs) |                 cls.instance = super(Singleton, cls).__call__(*args, **kwargs) | ||||||
|             return cls.instance |             return cls.instance | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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
									
								
							
							
						
						
									
										153
									
								
								cdist/integration.py
									
										
									
									
									
										Normal 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) | ||||||
|  | @ -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) | ||||||
|  |  | ||||||
							
								
								
									
										47
									
								
								docs/src/cdist-integration.rst
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								docs/src/cdist-integration.rst
									
										
									
									
									
										Normal 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') | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue