├── .gitignore ├── .travis.yml ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── NEWS ├── README.md ├── additional.conf ├── bump-vesion.sh ├── configsnap ├── configsnap.help2man ├── configsnap.spec ├── debian ├── compat ├── configsnap.install ├── control ├── rules └── source │ └── format ├── docker-compose.yml ├── dockerfiles ├── Dockerfile-buster ├── Dockerfile-el6 ├── Dockerfile-el7 ├── Dockerfile-el8 └── Dockerfile-fedora └── test_configsnap.py /.gitignore: -------------------------------------------------------------------------------- 1 | BUILD 2 | rpmbuild 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: required 3 | python: 4 | - 2.7 5 | install: 6 | - pip install flake8 7 | script: 8 | - flake8 configsnap --count --max-line-length=127 --show-source --statistics 9 | - sudo ./test_configsnap.py 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | configsnap is maintained by: 2 | ``` 3 | - Paolo Gigante 4 | - Jean-Yves Michaud 5 | ``` 6 | 7 | Original author at Rackspace (http://www.rackspace.co.uk): 8 | ``` 9 | - Cian Brennan 10 | ``` 11 | 12 | Major contributors: 13 | ``` 14 | - Cameron Beere 15 | - Piers Cornwell 16 | ``` 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for configsnap 2 | # Required packages: 3 | # RPM 4 | # rpmbuild 5 | # 6 | # DEB 7 | # build-essential 8 | # devscripts 9 | # 10 | # package details 11 | NAME = "configsnap" 12 | BUILD_FILES = configsnap additional.conf configsnap.help2man LICENSE README.md 13 | BUILD_FILES += MAINTAINERS.md NEWS 14 | 15 | SHELL = /bin/bash 16 | 17 | # package info 18 | UPSTREAM = "https://github.com/rackerlabs/$(NAME).git" 19 | VERSION := $(shell git tag -l | sort -V | tail -n 1) 20 | RELEASE := $(shell perl -nle 'print $$& while m{^Release:\s+\K[0-9]+}g' $(NAME).spec) 21 | COMMIT = $(shell git log --pretty=format:'%h' -n 1) 22 | DATE = $(shell date +%Y-%m-%d) 23 | DATELONG = $(shell date +%Y-%m-%dT%H:%M:%S%z) 24 | 25 | # build info 26 | BUILD_ROOT := "BUILD" 27 | BUILD_DIR := "$(NAME)-$(VERSION)" 28 | PATCH_DIR = $(CURDIR)/patches 29 | OS := $(shell uname) 30 | ifeq ($(OS), Darwin) 31 | DIST := "MacOS" 32 | else 33 | DIST := $(shell lsb_release -si) 34 | DIST_VER := $(shell lsb_release -sr | cut -d '.' -f1) 35 | endif 36 | DIST_DIR := $(DIST)$(DIST_VER) 37 | 38 | ifeq ($(DIST), $(filter $(DIST), Fedora CentOS)) 39 | RPM_TOPDIR := $(shell rpm -E '%{_topdir}') 40 | RPM_SPECDIR := $(shell rpm -E '%{_specdir}') 41 | RPM_SRCDIR := $(shell rpm -E '%{_sourcedir}') 42 | #RPM_RPMDIR := $(shell rpm -E '%{_rpmdir}') 43 | #RPM_SRPMDIR := $(shell rpm -E '%{_srcrpmdir}') 44 | endif 45 | 46 | ifeq ($(DIST), Debian) 47 | DEB_DIST := $(shell lsb_release -sc) 48 | endif 49 | 50 | .PHONY: deb rpm variables setup-build-dir prepare-patches clean ${BUILD_FILES} 51 | 52 | all: variables 53 | 54 | rpm: prepare-patches 55 | @echo "Building release $(VERSION)_$(RELEASE) for $(DIST_DIR)" 56 | cd $(BUILD_ROOT)/$(DIST_DIR) && \ 57 | tar -czvf $(VERSION).tar.gz $(BUILD_DIR) && \ 58 | cp $(VERSION).tar.gz $(RPM_SRCDIR)/ 59 | cp $(NAME).spec $(RPM_SPECDIR)/ 60 | rpmbuild -ba $(RPM_SPECDIR)/$(NAME).spec 61 | 62 | deb: prepare-patches 63 | @echo "Building release $(VERSION)_$(RELEASE) for $(DIST_DIR)" 64 | tar -C $(BUILD_ROOT)/$(DIST_DIR) -czf $(BUILD_ROOT)/$(DIST_DIR)/$(NAME)_$(VERSION).orig.tar.gz $(BUILD_DIR) 65 | cp -rpv debian $(BUILD_ROOT)/$(DIST_DIR)/$(BUILD_DIR) 66 | cd $(BUILD_ROOT)/$(DIST_DIR)/$(BUILD_DIR) && \ 67 | debchange -M --create --package $(NAME) --force-distribution -D $(DEB_DIST) -v $(VERSION)-$(RELEASE) $(NAME) $(VERSION)-$(RELEASE) && \ 68 | debuild -i -us -uc -b 69 | 70 | 71 | prepare-patches: setup-build-dir 72 | ifeq ($(DIST)$(DIST_VER), Debian10) 73 | sed -i 's#/bin/python$$#/bin/python3#g' $(BUILD_ROOT)/$(DIST_DIR)/$(BUILD_DIR)/configsnap 74 | endif 75 | 76 | setup-build-dir: $(BUILD_FILES) 77 | rm -rf $(BUILD_ROOT)/$(DIST_DIR) 78 | for file in $(BUILD_FILES); do \ 79 | install -D -T $$file $(BUILD_ROOT)/$(DIST_DIR)/$(BUILD_DIR)/$$file; \ 80 | done 81 | 82 | clean: 83 | shopt -s nullglob && \ 84 | rm -f -r $(CURDIR)/$(BUILD_ROOT)/* 85 | 86 | variables: 87 | @echo "OS: $(OS)" 88 | @echo "DIST: $(DIST)" 89 | @echo "DIST_VER: $(DIST_VER)" 90 | @echo "NAME: $(NAME)" 91 | @echo "VERSION: $(VERSION)" 92 | @echo "RELEASE: $(RELEASE)" 93 | @echo "COMMIT: $(COMMIT)" 94 | @echo "DATE: $(DATE)" 95 | @echo "DATELONG: $(DATELONG)" 96 | @echo "BUILD_DIR: $(BUILD_DIR)" 97 | ifeq ($(DIST), $(filter $(DIST), Fedora CentOS)) 98 | @echo "RPM_TOPDIR: $(RPM_TOPDIR)" 99 | @echo "RPM_SPECDIR: $(RPM_SPECDIR)" 100 | @echo "RPM_SRCDIR: $(RPM_SRCDIR)" 101 | endif 102 | 103 | # vim: noet: 104 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | configsnap NEWS 2 | 3 | 0.15 4 | * Added new function `copy_dir` to backup the content of an entire directory 5 | 6 | 0.14 7 | * Adjusted -w option to only overwrite specific tagged files 8 | * Add option to compare existing files without gathering new data using the -C/--compare-only option 9 | * Added the option to capture post data and compare to phases other than *.pre using the --pre option 10 | * Added option to force a compare even id the phase does not contain "post" or "rollback" using the --force-compare option 11 | 12 | 0.13 13 | * New option -a to create a tar archive of the output 14 | * New option -w to overwrite existing output 15 | * PEP8 fixes 16 | * Modify check for PHP presence 17 | 18 | 0.12 19 | * Record Pacemaker status 20 | * Don't raise exception if command doesn't exist 21 | * Add alternative path for lspci 22 | * Allow MySQL show databases to fail 23 | * Record PHP state 24 | * Record iptables rules 25 | * Documented tested platforms 26 | * Optional custom collection 27 | 28 | 0.11 29 | * Renamed from getData to configsnap 30 | * Backup grubenv for grub2 31 | * Support for Fedora 32 | * Added man page 33 | * Record dm-multipath information 34 | * Continue if lvm isn't present 35 | * Allow PowerPath to be present, but with no LUNs 36 | 37 | 0.10 38 | * Initial public release 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # configsnap 2 | 3 | Records useful system state information, and compare to previous state if run 4 | with PHASE containing "post" or "rollback". 5 | 6 | Tested and packaged for RHEL and it's derivatives through the EPEL repository. 7 | Packages are also supplied for recent Debian based systems, however they are 8 | less tested. 9 | 10 | ``` 11 | Usage: configsnap [options] 12 | 13 | Record useful system state information, and compare to previous state if run 14 | with PHASE containing "post" or "rollback". A default config file, 15 | /etc/configsnap/additional.conf, can be customised to include extra files, directories 16 | or commands to register during configsnap execution. 17 | 18 | Options: 19 | -h, --help show this help message and exit 20 | -w, --overwrite if phase files already exist in tag dir, remove 21 | previously collected data with that tag 22 | -a, --archive pack output files into a tar archive 23 | -v, --verbose print debug info 24 | -V, --version print version 25 | -s, --silent no output to stdout 26 | --force-compare Force a comparison after collecting data 27 | -t TAG, --tag=TAG tag identifer (e.g. a ticket number) 28 | -d BASEDIR, --basedir=BASEDIR 29 | base directory to store output 30 | -p PHASE, --phase=PHASE 31 | phase this is being used for. Can be any string. 32 | Phases containing post or rollback will perform 33 | diffs 34 | -C, --compare-only Compare existing files with tags specified with --pre 35 | and --phase 36 | --pre=PRE_SUFFIX suffix for files captured at previous state, for 37 | comparison 38 | -c CONFIG, --config=CONFIG 39 | additional config file to use. Setting this will 40 | override default. 41 | ``` 42 | 43 | Example output: 44 | ``` 45 | # ./configsnap -t junepatching -p pre 46 | Getting storage details (LVM, partitions, PowerPath)... 47 | Getting process list... 48 | Getting package list and enabled services... 49 | Getting network details and listening services... 50 | Getting cluster status... 51 | Getting misc (dmesg, lspci, sysctl)... 52 | Getting Dell hardware information... 53 | Copying files... 54 | /boot/grub/grub.conf 55 | /etc/fstab 56 | /etc/hosts 57 | /etc/sysconfig/network 58 | /etc/yum.conf 59 | /proc/cmdline 60 | /proc/cpuinfo 61 | /proc/meminfo 62 | /proc/mounts 63 | /proc/scsi/scsi 64 | /etc/sysconfig/network-scripts/ifcfg-eth3 65 | /etc/sysconfig/network-scripts/ifcfg-lo 66 | /etc/sysconfig/network-scripts/ifcfg-eth1 67 | /etc/sysconfig/network-scripts/ifcfg-eth0 68 | /etc/sysconfig/network-scripts/ifcfg-eth2 69 | /etc/sysconfig/network-scripts/route-eth2 70 | 71 | 72 | Finished! Backups were saved to /root/junepatching/configsnap/*.pre 73 | ``` 74 | 75 | Custom collection of additional command output (Type: command) and files (Type: 76 | file) can be configured in the file /etc/configsnap/additional.conf, for 77 | example: 78 | 79 | ``` 80 | [psspecial] 81 | Type: command 82 | Command: /bin/ps -aux 83 | Compare: True 84 | # Recording the output of a command into a "psspecial." file containing the output. 85 | 86 | [debconf.conf] 87 | Type: file 88 | File: /etc/debconf.conf 89 | Failok: True 90 | # Recording an additional file, stored as "debconf." 91 | 92 | [ssh] 93 | Type: directory 94 | Directory: /etc/ssh/ 95 | # Recursively Recording all files from /etc/ssh/ directory, with sub-files appended with ".". 96 | 97 | [fail2ban] 98 | Type: directory 99 | Directory: /etc/fail2ban 100 | File_Pattern: .*\.local$ 101 | # Recording all files from /etc/fail2ban/ directory matching '.*\.local$', with sub-files 102 | appended with "." 103 | ``` 104 | 105 | -------------------------------------------------------------------------------- /additional.conf: -------------------------------------------------------------------------------- 1 | # Optional custom command or file collection for configsnap 2 | # 3 | # Format: 4 | # [section] 5 | # Type: file 6 | # File: [ source filename ] 7 | # 8 | # [section] 9 | # Type: directory 10 | # Directory: [ full command to run ] 11 | # File_Pattern: [ regular expression to match filenames ] 12 | # 13 | # [section] 14 | # Type: command 15 | # Command: [ full command to run ] 16 | # Sort: [ True | False ] (optional) 17 | # 18 | # The following options apply to either "file" or "command": 19 | # Failok: [ True | False ] - what to do if action fails (optional) 20 | # Compare: [ True | False ] - when phase contains post or rollback, whether to diff against pre (optional) 21 | # 22 | # 23 | # Examples: 24 | # [psspecial] 25 | # Type: command 26 | # Command: /bin/ps -aux 27 | # 28 | # [debconf.conf] 29 | # Type: file 30 | # File: /etc/debconf.conf 31 | # 32 | # [ssh] 33 | # Type: directory 34 | # Directory: /etc/ssh/ 35 | # File_Pattern: sshd.* 36 | -------------------------------------------------------------------------------- /bump-vesion.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | NVER="${1:?Missing version number}" 4 | 5 | sed -ri 's/(version\s+=\s+).*/\1"'"$NVER"'"/' configsnap 6 | sed -ri 's/(Version:\s+)[0-9]+\..*/\1'"$NVER"'/' configsnap.spec 7 | 8 | vim configsnap.spec 9 | -------------------------------------------------------------------------------- /configsnap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2016-2021 Rackspace, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use 6 | # this file except in compliance with the License. You may obtain a copy of the 7 | # License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software distributed 12 | # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 13 | # CONDITIONS OF ANY KIND, either express or implied. See the License for the 14 | # specific language governing permissions and limitations under the License. 15 | from __future__ import print_function 16 | try: 17 | import configparser 18 | except ImportError: 19 | # Python2 compatibility 20 | import ConfigParser as configparser 21 | import os 22 | import os.path 23 | import optparse 24 | import sys 25 | import subprocess 26 | import datetime 27 | import glob 28 | import re 29 | import tarfile 30 | 31 | version = "0.21.1" 32 | diffs_found = False 33 | 34 | 35 | def report_verbose(message): 36 | if options.verbose_enabled: 37 | sys.stdout.write("\033[92m%s\n\033[0m" % message) 38 | 39 | 40 | def report_info(message): 41 | if not options.silent_enabled: 42 | sys.stdout.write("\033[92m%s\n\033[0m" % message) 43 | 44 | 45 | def report_info_blue(message): 46 | if not options.silent_enabled: 47 | sys.stdout.write("\033[1;34m%s\n\033[0m" % message) 48 | 49 | 50 | def report_error(message): 51 | if not options.silent_enabled: 52 | sys.stdout.write("\033[91m%s\n\033[0m" % message) 53 | 54 | 55 | def check_option_conflicts(options): 56 | # Do not allow silent and verbose options to be used in conjunction 57 | if options.silent_enabled is True and options.verbose_enabled is True: 58 | options.silent_enabled = False 59 | report_error("Conflicting options provided: '-v' and '-s' are not compatible") 60 | sys.exit(1) 61 | 62 | # Do not allow archive or overwrite to be used in conjunction with compare-only 63 | if options.compare_only is True: 64 | conflicts = [options.silent_enabled is True, 65 | options.archive_enabled is True, 66 | options.overwrite_enabled is True] 67 | 68 | if (options.phase is None or options.pre_suffix is None): 69 | report_error("--phase or --pre not set") 70 | sys.exit(1) 71 | 72 | if any(conflicts): 73 | options.silent_enabled = False 74 | report_error("Conflicting options provided: '-a', '-s' and '-w' " 75 | "conflict with '-C'") 76 | sys.exit(1) 77 | 78 | 79 | def create_dir(tagdir, overwrite): 80 | workdir = os.path.join(tagdir, "configsnap") 81 | if os.path.isdir(workdir) and overwrite: 82 | try: 83 | # We need to go through all subdirs as well 84 | for root, dirs, files in os.walk(workdir): 85 | if any(s.endswith(".%s" % options.phase) for s in files): 86 | report_info("%s files exist in %s. Overwrite " 87 | "(-w/--overwrite) enabled so removing." % (options.phase, root)) 88 | 89 | # actual removal 90 | for filename in files: 91 | if filename.endswith(".%s" % options.phase): 92 | os.remove(os.path.join(root, filename)) 93 | 94 | except OSError as e: 95 | report_error("Unable to remove %s: %s" % (workdir, e)) 96 | sys.exit(1) 97 | 98 | if not os.path.isdir(workdir): 99 | try: 100 | os.makedirs(workdir) 101 | except OSError as e: 102 | report_error("Unable to create %s: %s" % (workdir, e)) 103 | sys.exit(1) 104 | 105 | return workdir 106 | 107 | 108 | def create_archive(tagdir, workdir, tag, overwrite): 109 | """Create tar archive of workdir and return filename and success""" 110 | output_filename = "%s/configsnap-%s.tar.gz" % (tagdir, tag) 111 | 112 | if os.path.isfile(output_filename): 113 | if overwrite: 114 | report_info( 115 | "%s exists. Overwrite (-w/--overwrite) enabled so will overwrite." % output_filename) 116 | else: 117 | report_error( 118 | "Archive file %s already exists. Use -w overwrite." % output_filename) 119 | return "", False 120 | 121 | try: 122 | tar = tarfile.open(output_filename, "w:gz") 123 | tar.add(workdir, arcname=os.path.basename(workdir)) 124 | tar.close() 125 | except IOError as e: 126 | report_error("Can't create archive %s: %s" % (output_filename, e)) 127 | sys.exit(1) 128 | 129 | return output_filename, True 130 | 131 | 132 | def compare_files(options): 133 | """Compare 2 phases of a same tag""" 134 | run = dataGather(workdir, phase) 135 | 136 | # Collects all files and dir at the top level 137 | all_files = os.listdir(workdir) 138 | # Those will always result in positive diffs, this is misleading so we exclude them 139 | exclusions = '^(sysctl|vgcfgbackup[-].*|ps|meminfo|dmesg|crm_mon)$' 140 | 141 | # Proceed if at least one file with options.pre_suffix is present 142 | if any(filename.endswith(".%s" % options.pre_suffix) for filename in all_files): 143 | report_info("Perfoming file diffs...\n") 144 | 145 | if 'rollback' in phase: 146 | run.get_diff_files('packages', options) 147 | 148 | # to search for files with pre_suffix or phase only 149 | suffix_exist = re.compile('[.](' + options.pre_suffix + '|' + options.phase + ')$') 150 | filtered_files = [] 151 | for index, value in enumerate(all_files): 152 | if suffix_exist.search(value): 153 | filebase = suffix_exist.sub('', value) 154 | if not re.match(exclusions, filebase): 155 | filtered_files.append(filebase) 156 | 157 | compare_files = list(set(filtered_files)) 158 | compare_files.sort() 159 | 160 | for filename in compare_files: 161 | run.get_diff_files(filename, options) 162 | 163 | # Report subdirectories at the subdir level unless ther verbose option is 164 | # given, in which case report as usual 165 | report_info("\nStarting sub-dir diffs...") 166 | for directory in os.listdir(workdir): 167 | if os.path.isdir(os.path.join(workdir, directory)): 168 | run.get_diff_dir(directory, options) 169 | 170 | try: 171 | uname_pre_fd = open( 172 | os.path.join(workdir, 'uname.' + options.pre_suffix), 'r') 173 | uname_post_fd = open( 174 | os.path.join(workdir, "uname.%s" % phase), 'r') 175 | uname_pre = uname_pre_fd.read().strip() 176 | uname_post = uname_post_fd.read().strip() 177 | if uname_pre != uname_post: 178 | sys.stdout.write("Old uname: %s. New uname: %s\n" % 179 | (uname_pre, uname_post)) 180 | except Exception as e: 181 | report_error( 182 | "Unable to open file %s: %s" % ((e.filename, e.strerror))) 183 | 184 | if diffs_found: 185 | report_info_blue("\nINTERPRETING DIFFS:\n") 186 | report_info_blue("* Lines beginning with a - show an " 187 | "entry from the " + options.pre_suffix + " pre file " 188 | "which has changed\n" "or which is not present in the .post or .rollback file.\n") 189 | report_info_blue("* Lines beginning with a + show an entry from the " 190 | ".post or.rollback file which\nhas changed or which " 191 | "is not present in the " + options.pre_suffix + " pre file.\n") 192 | report_info_blue("Please review all diff output above carefully and " 193 | "account for any difference found.\n") 194 | 195 | else: 196 | report_error("Unable to diff, no files with suffix \"" + 197 | options.pre_suffix + "\" present.") 198 | sys.exit(1) 199 | 200 | 201 | class dataGather: 202 | 203 | def copy_file(self, r_filename, destdir=None, fail_ok=False, sort=False): 204 | # Did we override the destination 205 | if destdir is None: 206 | destdir = self.workdir 207 | 208 | if not os.path.exists(destdir): 209 | try: 210 | os.makedirs(destdir) 211 | except OSError as e: 212 | report_error("Unable to create directory %s: %s" % (workdir, e)) 213 | sys.exit(1) 214 | 215 | if not os.path.exists(r_filename): 216 | if not fail_ok: 217 | report_error(" %s does not exist" % r_filename) 218 | return 219 | filename = os.path.basename(r_filename) 220 | w_filename = os.path.join(destdir, "%s.%s" % 221 | (filename, self.phase)) 222 | 223 | try: 224 | r_fd = open(r_filename, 'r') 225 | w_fd = open(w_filename, 'w') 226 | 227 | if sort is False: 228 | w_fd.write(r_fd.read()) 229 | else: 230 | data = r_fd.readlines() 231 | data.sort() 232 | w_fd.writelines(data) 233 | 234 | r_fd.close() 235 | w_fd.close() 236 | if not options.silent_enabled: 237 | sys.stdout.write(" %s \n" % r_filename) 238 | 239 | except IOError: 240 | report_error("Error copying %s" % w_filename) 241 | 242 | def copy_dir(self, srcdir, file_pattern='.*', destdir=None, fail_ok=False, sort=False): 243 | # Did we override the destination 244 | if destdir is None: 245 | destdir = os.path.join( 246 | self.workdir, os.path.basename(os.path.dirname(srcdir))) 247 | 248 | if not os.path.exists(srcdir): 249 | if not fail_ok: 250 | report_error(" Directory %s does not exist" % destdir) 251 | return 252 | 253 | for root, dirs, files in os.walk(srcdir): 254 | for cfile in files: 255 | if re.match(file_pattern, cfile) and not os.path.islink(os.path.join(root, cfile)): 256 | self.copy_file(os.path.join(root, cfile), 257 | destdir=os.path.join( 258 | destdir, root.replace(srcdir, '')), 259 | fail_ok=False, 260 | sort=False) 261 | 262 | def run_command(self, command, filename, fail_ok=False, sort=False, stdout=False, shell=False, regexp_filter=None): 263 | 264 | try: 265 | cmd_proc = subprocess.Popen( 266 | command, stdout=subprocess.PIPE, 267 | stderr=subprocess.PIPE, shell=shell) 268 | 269 | except OSError as e: 270 | if not fail_ok: 271 | report_error("Error running %s: %s" % 272 | (command[0], e.strerror)) 273 | return False 274 | 275 | # Put both stdout and stderr in separate strings 276 | cmd_stdout, cmd_stderr = cmd_proc.communicate() 277 | if not isinstance(cmd_stdout, str): 278 | # Python3 returns bytes 279 | cmd_stdout, cmd_stderr = cmd_stdout.decode('utf8'), cmd_stderr.decode('utf8') 280 | 281 | # Convert stdout to a list and preserve the newlines 282 | cmd_stdout = cmd_stdout.splitlines(True) 283 | 284 | returncode = cmd_proc.wait() 285 | if not fail_ok and returncode != 0: 286 | report_error("%s failed\nPlease troubleshoot manually" % 287 | (' '.join(command), )) 288 | return False 289 | 290 | if len(cmd_stdout) == 0: 291 | return True 292 | 293 | if sort: 294 | cmd_stdout.sort() 295 | 296 | # Filtering the output before storing into file, if applicable 297 | if regexp_filter is not None: 298 | myfilter = re.compile(regexp_filter) 299 | cmd_stdout = list(filter(myfilter.match, cmd_stdout)) 300 | 301 | filename = os.path.join(self.workdir, "%s.%s" % (filename, self.phase)) 302 | 303 | if os.path.exists(filename): 304 | report_error("%s already exists" % filename) 305 | 306 | try: 307 | with open(filename, 'w') as output_fd: 308 | output_fd.writelines(cmd_stdout) 309 | report_verbose("Recording %s to %s" % (command, filename)) 310 | 311 | if stdout: 312 | sys.stdout.writelines(cmd_stdout) 313 | 314 | except IOError as e: 315 | report_error("Unable to open %s: %s" % (filename, e.strerror)) 316 | return False 317 | 318 | def get_diff_files(self, filename, options): 319 | filename = os.path.join(self.workdir, filename) 320 | pre_filename = "%s.%s" % (filename, options.pre_suffix) 321 | post_filename = "%s.%s" % (filename, self.phase) 322 | 323 | try: 324 | diff_proc = subprocess.Popen(['diff', '--unified=0', 325 | '--ignore-blank-lines', 326 | '--ignore-space-change', 327 | pre_filename, 328 | post_filename], 329 | stdout=subprocess.PIPE, 330 | stderr=subprocess.PIPE, 331 | shell=False) 332 | except OSError as e: 333 | report_error("Error running diff: %s" % 334 | (e.strerror)) 335 | return False 336 | 337 | diff_stdout, diff_stderr = diff_proc.communicate(input=None) 338 | if not isinstance(diff_stdout, str): 339 | # Python3 returns bytes 340 | diff_stdout, diff_stderr = diff_stdout.decode('utf8'), diff_stderr.decode('utf8') 341 | 342 | if diff_proc.returncode != 0: 343 | global diffs_found 344 | diffs_found = True 345 | report_error( 346 | "Differences found against %s.%s:\n" % (filename, options.pre_suffix)) 347 | if diff_stdout != "": 348 | sys.stdout.writelines(diff_stdout) 349 | else: 350 | report_error(diff_stderr) 351 | else: 352 | report_info("No differences against %s.%s" % 353 | (filename, options.pre_suffix)) 354 | 355 | def get_diff_dir(self, sdir, options): 356 | all_pre_files = [] 357 | sdir = os.path.join(workdir, sdir) 358 | 359 | for pre_filename in glob.glob(sdir + '/*' + options.pre_suffix): 360 | 361 | # building list of all 'pre' files 362 | bare_file = os.path.splitext(pre_filename)[0] + '.' + self.phase 363 | all_pre_files.append(bare_file) 364 | 365 | post_filename = "%s.%s" % (os.path.splitext(pre_filename)[0], 366 | self.phase) 367 | 368 | try: 369 | post_fd = open(post_filename) 370 | post_fd.close() 371 | except IOError as e: 372 | report_error("- File removed: %s" % (e.filename)) 373 | continue 374 | 375 | try: 376 | diff_proc = subprocess.Popen(['diff', '--unified=0', 377 | '--ignore-blank-lines', 378 | '--ignore-space-change', 379 | pre_filename, post_filename], 380 | stdout=subprocess.PIPE, 381 | stderr=subprocess.PIPE, 382 | shell=False) 383 | except OSError as e: 384 | report_error("Error running diff: %s" % 385 | (e.strerror)) 386 | return None 387 | 388 | if diff_proc.wait() != 0: 389 | global diffs_found 390 | diffs_found = True 391 | report_error("Differences found against %s:\n" % pre_filename) 392 | sys.stdout.writelines(diff_proc.stdout.readlines()) 393 | elif options.verbose_enabled is True: 394 | report_verbose("No differences against %s" % pre_filename) 395 | 396 | # Looking for new files 397 | local_diffs = False 398 | for post_filename in glob.glob(sdir + '/*' + self.phase): 399 | if post_filename not in all_pre_files: 400 | report_error("+ File added: %s:" % post_filename) 401 | local_diffs = True 402 | 403 | if local_diffs is False: 404 | report_info("No extra post files found in %s" % sdir) 405 | 406 | def is_running(self, *paths): 407 | if len(self.procs_out) == 0: 408 | # Get a list of processes running on the server 409 | procs = subprocess.Popen( 410 | ['ps', '-A', '--noheaders', '-o', 'args'], stdout=subprocess.PIPE, 411 | stderr=subprocess.PIPE) 412 | 413 | procs_out, procs_err = procs.communicate() 414 | if not isinstance(procs_out, str): 415 | # Python3 returns bytes 416 | procs_out, procs_err = procs_out.decode('utf8'), procs_err.decode('utf8') 417 | if procs.returncode == 0: 418 | self.procs_out = [x.split()[0] for x in procs_out.splitlines()] 419 | else: 420 | report_error('Failed to get process list') 421 | 422 | for path in paths: 423 | if path in self.procs_out: 424 | return True 425 | 426 | return False 427 | 428 | def __init__(self, workdir, phase): 429 | self.workdir = workdir 430 | self.phase = phase 431 | self.procs_out = [] 432 | 433 | 434 | ################################################### 435 | # 436 | # MAIN 437 | # 438 | ################################################### 439 | 440 | desc = ("Record useful system state information, and compare to previous state " 441 | "if run with PHASE containing \"post\" or \"rollback\".\nAn optional file, " 442 | "/etc/configsnap/additional.conf, can be provided for extra files, " 443 | "directories or commands to register during configsnap execution.") 444 | 445 | parser = optparse.OptionParser(description=desc) 446 | parser.add_option('-w', '--overwrite', 447 | action="store_true", dest="overwrite_enabled", default=False, 448 | help='if phase files already exist in tag dir, remove previously collected data with that tag') 449 | parser.add_option('-a', '--archive', 450 | action="store_true", dest="archive_enabled", default=False, 451 | help='pack output files into a tar archive') 452 | parser.add_option("-v", "--verbose", 453 | action="store_true", dest="verbose_enabled", default=False, 454 | help="print debug info") 455 | parser.add_option("-V", "--version", 456 | action="store_true", dest="print_version", default=False, 457 | help="print version") 458 | parser.add_option("-s", "--silent", 459 | action="store_true", dest="silent_enabled", default=False, 460 | help="no output to stdout") 461 | parser.add_option("--force-compare", 462 | action="store_true", dest="force_compare", default=False, 463 | help="Force a comparison after collecting data") 464 | parser.add_option('-t', '--tag', 465 | dest='tag', 466 | help='tag identifer (e.g. a ticket number)') 467 | parser.add_option('-d', '--basedir', 468 | dest='basedir', default='/root', 469 | help='base directory to store output') 470 | parser.add_option('-p', '--phase', 471 | dest='phase', 472 | help='phase this is being used for. ' 473 | 'Can be any string. Phases containing post or rollback will perform diffs') 474 | parser.add_option('-C', '--compare-only', 475 | action="store_true", dest='compare_only', default=False, 476 | help='Compare existing files with tags specified with --pre and --phase') 477 | parser.add_option('--pre', 478 | dest='pre_suffix', default='pre', 479 | help='suffix for files captured at previous state, for comparison') 480 | parser.add_option('-c', '--config', default='/etc/configsnap/additional.conf', 481 | help='additional config file to use. Setting this will ' 482 | 'overwrite default.') 483 | 484 | (options, args) = parser.parse_args() 485 | 486 | if options.print_version: 487 | print("configsnap %s" % version) 488 | sys.exit(0) 489 | 490 | if os.geteuid() != 0: 491 | report_error("Not running as root, exiting") 492 | sys.exit(1) 493 | 494 | if not options.tag: 495 | report_error("No tag given, exiting") 496 | sys.exit(1) 497 | 498 | check_option_conflicts(options) 499 | 500 | # Load custom collection list 501 | Config = configparser.ConfigParser() 502 | # Grab the full path before we chdir 503 | customcollectionfile = os.path.abspath(options.config) 504 | 505 | 506 | if os.path.exists(customcollectionfile): 507 | # Check file is owned by root and not read/writable by anyone else 508 | st = os.stat(customcollectionfile) 509 | if st.st_uid != 0: 510 | report_error("Custom collection file %s not owned by root, ignoring" % 511 | customcollectionfile) 512 | sys.exit(1) 513 | elif int(oct(st.st_mode)[-2:]) > 44: 514 | report_error("Custom collection file %s is writable by non-root users, ignoring" % 515 | customcollectionfile) 516 | sys.exit(1) 517 | else: 518 | # Only read in the config file if the above checks have passed 519 | Config.read(customcollectionfile) 520 | 521 | # Does the default extra config file exist? Warn if yes 522 | if (os.path.exists('/etc/configsnap/additional.conf') and 523 | customcollectionfile != '/etc/configsnap/additional.conf'): 524 | report_info_blue("Default config file /etc/configsnap/" 525 | "additional.conf will be ignored, using %s" 526 | % customcollectionfile) 527 | 528 | elif customcollectionfile != '/etc/configsnap/additional.conf': 529 | report_error("Additional config file %s not found! Quitting." 530 | % customcollectionfile) 531 | sys.exit(1) 532 | 533 | # os.path.abspath to deal with relavive paths 534 | tagdir = os.path.join(os.path.abspath(options.basedir), options.tag) 535 | workdir = create_dir(tagdir, options.overwrite_enabled) 536 | os.chdir(workdir) 537 | 538 | if "PATH" not in os.environ: 539 | os.environ['PATH'] = '/usr/bin:/bin' 540 | os.environ['PATH'] += ':/usr/sbin:/sbin' 541 | 542 | if options.phase: 543 | phase = options.phase 544 | # We need to go into subdirs as well for checking 545 | for root, dirs, files in os.walk(workdir): 546 | for filename in files: 547 | if filename.endswith(".%s" % phase) and not options.compare_only: 548 | report_error("Files for %s already exist in %s" 549 | % (phase, os.getcwd())) 550 | sys.exit(1) 551 | else: 552 | now = datetime.datetime.now() 553 | phase = "%d%02d%02d%02d%02d" % (now.year, now.month, now.day, 554 | now.hour, now.minute) 555 | 556 | if options.compare_only: 557 | report_info("Comparing files from phases: %s and %s" % 558 | (options.pre_suffix, options.phase)) 559 | compare_files(options) 560 | sys.exit(0) 561 | 562 | # Ensure each section has a valid Type 563 | for section in Config.sections(): 564 | if Config.has_option(section, 'type'): 565 | if not re.match('^(file|directory|command)$', Config.get(section, 'type').lower()): 566 | report_error("Wrong \"Type\" defined for custom collection entry \"%s\"; " 567 | "should be \"file\", \"directory\" or \"command\"" % section) 568 | 569 | run = dataGather(workdir, phase) 570 | report_info("Getting storage details (LVM, partitions, multipathing, lsblk, blkid)...") 571 | if os.path.exists('/sbin/lvs'): 572 | run.run_command(['lvs', '--noheadings'], 'lvs', fail_ok=True, sort=True) 573 | run.run_command(['vgs', '--noheadings'], 'vgs', fail_ok=True, sort=True) 574 | run.run_command(['pvs', '--noheadings'], 'pvs', fail_ok=True, sort=True) 575 | 576 | try: 577 | vgcfg = subprocess.Popen(['vgcfgbackup', '-f', 578 | os.path.join(workdir, 579 | "vgcfgbackup-%%s.%s" % phase)], 580 | stdout=subprocess.PIPE, 581 | stderr=subprocess.PIPE) 582 | vgcfg.wait() 583 | except OSError as e: 584 | report_error("Error running vgcfgbackup: %s" % e.strerror) 585 | else: 586 | if vgcfg.returncode != 0: 587 | report_error("vgcfg exited with return code %d" % vgcfg.returncode) 588 | 589 | run.run_command(['parted', '-l', '-s'], 590 | 'partitions', fail_ok=False, sort=False) 591 | if os.path.exists('/etc/multipath.conf'): 592 | run.run_command(['multipath', '-l'], 'multipath', fail_ok=True, sort=False) 593 | if os.path.exists('/sbin/powermt'): 594 | run.run_command(['powermt', 'display', 'dev=all'], 595 | 'powermt', fail_ok=True, sort=False, stdout=False) 596 | if os.path.exists('/usr/bin/lsblk'): 597 | run.run_command(['lsblk'], 'lsblk', fail_ok=True, sort=False) 598 | if os.path.exists('/usr/bin/blkid'): 599 | run.run_command(['blkid'], 'blkid', fail_ok=True, sort=False) 600 | 601 | report_info("Getting process list...") 602 | run.run_command(['ps', 'a', 'u', 'f', 'x'], 'ps', fail_ok=False, sort=False) 603 | 604 | report_info("Getting package list and enabled services...") 605 | run.run_command(['uname', '-r'], 'uname', fail_ok=False, sort=False) 606 | if os.path.exists('/bin/rpm'): 607 | run.run_command(['rpm', '-qa'], 'packages', fail_ok=False, sort=True) 608 | if os.path.exists('/usr/bin/dpkg'): 609 | run.run_command(['dpkg', '-l'], 'packages', fail_ok=False, sort=False) 610 | if os.path.exists('/sbin/chkconfig'): 611 | run.run_command(['chkconfig', '--list'], 612 | 'sysvinit', fail_ok=False, sort=True) 613 | if os.path.exists('/usr/sbin/sysv-rc-conf'): 614 | run.run_command( 615 | ['sysv-rc-conf', '--list'], 'sysvinit', fail_ok=True, sort=True) 616 | if os.path.exists('/sbin/initctl'): 617 | run.run_command(['initctl', 'list'], 618 | 'upstartinit', fail_ok=True, sort=True) 619 | if os.path.exists('/usr/bin/systemctl'): 620 | run.run_command(['systemctl', 'list-unit-files'], 621 | 'systemdinit', fail_ok=True, sort=False) 622 | 623 | report_info( 624 | "Getting network details, firewall rules and listening services...") 625 | run.run_command(['ip', 'a', 's'], 626 | 'ip_addresses', fail_ok=False, sort=False, 627 | regexp_filter=r'^(?!\s+valid_lft).*') 628 | run.run_command(['ip', 'route', 'show'], 629 | 'ip_routes', fail_ok=False, sort=False) 630 | run.run_command(['iptables', '--list-rules'], 631 | 'iptables', fail_ok=True, sort=False) 632 | run.run_command( 633 | "netstat -nutlp|awk -F'[ /]+' '/tcp/ {print $8,$1,$4}'|column -t", 634 | 'netstat', fail_ok=False, sort=True, shell=True) 635 | 636 | report_info("Getting cluster status...") 637 | if os.path.exists('/usr/sbin/clustat'): 638 | run.run_command(['clustat', '-l'], 'clustat', 639 | fail_ok=False, sort=False, stdout=False) 640 | if os.path.exists('/usr/sbin/pcs'): 641 | run.run_command(['pcs', 'status'], 'pcs_status', 642 | fail_ok=False, sort=False, stdout=False, 643 | regexp_filter=r'^(?!Last updated).*') 644 | run.run_command(['pcs', 'constraint', 'show', '--full'], 'pcs_constraints', 645 | fail_ok=False, sort=False, stdout=False) 646 | if os.path.exists('/sbin/crm_mon'): 647 | run.run_command(['crm_mon', '--as-xml'], 'crm_mon', 648 | fail_ok=False, sort=False, stdout=False) 649 | 650 | report_info("Getting misc (dmesg, lspci, sysctl)...") 651 | run.run_command(['dmesg'], 'dmesg', fail_ok=False, sort=False) 652 | if os.path.exists('/sbin/lspci') or os.path.exists('/usr/bin/lspci'): 653 | run.run_command(['lspci', '-mm'], 'lspci', fail_ok=True, sort=True) 654 | 655 | run.run_command(['sysctl', '-a'], 'sysctl', fail_ok=True, sort=True) 656 | 657 | omreportpath = '/opt/dell/srvadmin/bin/omreport' 658 | if os.path.exists(omreportpath): 659 | report_info("Getting Dell hardware information...") 660 | run.run_command([omreportpath, 'chassis'], 661 | 'omreport_chassis', fail_ok=True, 662 | sort=False, stdout=False) 663 | run.run_command([omreportpath, 'chassis', 'biossetup'], 664 | 'omreport_biossetup', fail_ok=True, 665 | sort=False, stdout=False) 666 | run.run_command([omreportpath, 'system', 'version'], 667 | 'omreport_version', fail_ok=True, 668 | sort=False, stdout=False) 669 | run.run_command([omreportpath, 'chassis', 'memory'], 670 | 'omreport_memory', fail_ok=True, 671 | sort=False, stdout=False) 672 | run.run_command([omreportpath, 'chassis', 'processors'], 673 | 'omreport_processors', fail_ok=True, 674 | sort=False, stdout=False) 675 | run.run_command([omreportpath, 'chassis', 'nics'], 676 | 'omreport_nics', fail_ok=True, 677 | sort=False, stdout=False) 678 | run.run_command([omreportpath, 'storage', 'vdisk'], 679 | 'omreport_vdisk', fail_ok=True, 680 | sort=False, stdout=False) 681 | run.run_command([omreportpath, 'storage', 'pdisk', 'controller=0'], 682 | 'omreport_pdisk', fail_ok=True, sort=False, stdout=False) 683 | run.run_command([omreportpath, 'storage', 'battery'], 684 | 'omreport_raidbattery', fail_ok=True, sort=False, stdout=False) 685 | 686 | hpasmpath = '/sbin/hpasmcli' 687 | dmipath = '/usr/sbin/dmidecode' 688 | hpstorutil = ['/usr/sbin/hpssacli', '/usr/sbin/hpacucli'] 689 | if os.path.exists(hpasmpath): 690 | report_info("Getting HP hardware information...") 691 | run.run_command([hpasmpath, '-s', 'show server'], 692 | 'hpreport_server', fail_ok=True, 693 | sort=False, stdout=False) 694 | run.run_command([hpasmpath, '-s', 'show dimm'], 695 | 'hpreport_memory', fail_ok=True, 696 | sort=False, stdout=False) 697 | if os.path.exists(dmipath): 698 | run.run_command([dmipath, '-t', 'bios'], 699 | 'hpreport_bios', fail_ok=True, 700 | sort=False, stdout=False) 701 | for storutil in hpstorutil: 702 | if os.path.exists(storutil): 703 | report_info("Getting HP storage data...") 704 | run.run_command([storutil, 'controller', 'all', 'show', 'detail'], 705 | 'hpreport_storage_controller', fail_ok=True, 706 | sort=False, stdout=False) 707 | # Grab the ID of the first controller reported 708 | # This returns something like: 709 | # "Smart Array P400 in Slot 1" 710 | stor_cmd = subprocess.Popen( 711 | [storutil, 'controller', 'all', 'show'], 712 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 713 | stor_out = stor_cmd.stdout.readlines() 714 | stor_cmd.wait() 715 | for o in stor_out: 716 | m = re.search('lot ([0-9]+)', o) 717 | if m: 718 | run.run_command( 719 | [storutil, 'controller', "slot=%s" % 720 | m.group( 721 | 1), 'logicaldrive', 'all', 'show', 'detail'], 722 | 'hpreport_storage_vdisk', fail_ok=True, sort=False, stdout=False) 723 | run.run_command( 724 | [storutil, 'controller', "slot=%s" % 725 | m.group( 726 | 1), 'physicaldrive', 'all', 'show', 'detail'], 727 | 'hpreport_storage_pdisk', fail_ok=True, sort=False, stdout=False) 728 | break 729 | break 730 | 731 | if run.is_running('/usr/sbin/httpd', '/usr/sbin/httpd.worker'): 732 | report_info("Getting Apache vhosts and modules...") 733 | run.run_command( 734 | ['httpd', '-S'], 'httpd_vhosts', fail_ok=False, sort=False) 735 | run.run_command( 736 | ['httpd', '-M'], 'httpd_modules', fail_ok=False, sort=True) 737 | 738 | if run.is_running('/usr/libexec/mysqld', '/usr/sbin/mysqld'): 739 | report_info("Getting MySQL databases...") 740 | run.run_command(['mysql', '-Bse', 'show databases;'], 741 | 'mysql_databases', fail_ok=True, sort=True) 742 | 743 | # Run any custom commands 744 | for section in Config.sections(): 745 | try: 746 | cfgtype = Config.get(section, 'type').lower() 747 | if cfgtype == 'command': 748 | cmd = Config.get(section, 'command') 749 | 750 | # Optional options 751 | if Config.has_option(section, 'failok') and Config.get(section, 'failok').lower() == "true": 752 | failokcfg = True 753 | # If failok is anything except true... 754 | else: 755 | failokcfg = False 756 | 757 | if Config.has_option(section, 'sort') and Config.get(section, 'sort').lower() == "true": 758 | sortcfg = True 759 | # If sort is anything except true... 760 | else: 761 | sortcfg = False 762 | 763 | report_info("Running custom command %s: %s" % (section, cmd)) 764 | run.run_command( 765 | cmd.split(), section, fail_ok=failokcfg, sort=sortcfg) 766 | except configparser.NoOptionError as e: 767 | report_error( 768 | "Check config file options for section %s: %s" % (section, e)) 769 | except Exception as e: 770 | report_error( 771 | "Could not parse config file section %s: %s" % (section, e)) 772 | 773 | # copy important files 774 | report_info("Copying files...") 775 | run.copy_file('/boot/grub/grub.conf', fail_ok=True) 776 | run.copy_file('/boot/grub2/grubenv', fail_ok=True) 777 | run.copy_file('/etc/default/grub', fail_ok=True) 778 | run.copy_file('/etc/fstab', fail_ok=False) 779 | run.copy_file('/etc/hosts', fail_ok=False) 780 | run.copy_file('/etc/sysconfig/network', fail_ok=True) 781 | run.copy_file('/etc/network/interfaces', fail_ok=True) 782 | run.copy_file('/etc/cluster/cluster.conf', fail_ok=True) 783 | run.copy_file('/etc/yum.conf', fail_ok=True) 784 | run.copy_file('/etc/dnf/dnf.conf', fail_ok=True) 785 | run.copy_file('/etc/apt/sources.list', fail_ok=True) 786 | run.copy_file('/proc/cmdline', fail_ok=False) 787 | run.copy_file('/proc/cpuinfo', fail_ok=False) 788 | run.copy_file('/proc/meminfo', fail_ok=False) 789 | run.copy_file('/proc/mounts', fail_ok=False, sort=False) 790 | run.copy_file('/proc/scsi/scsi', fail_ok=False, sort=False) 791 | run.copy_file('/etc/sudoers', fail_ok=False, sort=False) 792 | run.copy_dir('/etc/sudoers.d/', fail_ok=False, sort=False) 793 | if os.path.exists('/etc/network/interfaces.d'): 794 | run.copy_dir('/etc/network/interfaces.d/', fail_ok=False, sort=False) 795 | 796 | if os.path.exists('/etc/sysconfig/network-scripts/'): 797 | run.copy_dir('/etc/sysconfig/network-scripts/', file_pattern='(ifcfg-|route-).*', 798 | fail_ok=False, sort=False) 799 | # php specifics 800 | if os.path.exists('/usr/bin/php'): 801 | report_info("Copying PHP related files...") 802 | run.copy_file('/etc/php.ini', fail_ok=True) 803 | if os.path.exists('/etc/php.d'): 804 | run.copy_dir('/etc/php.d/', fail_ok=False, sort=False) 805 | 806 | if os.path.exists('/etc/php/'): 807 | run.copy_dir('/etc/php/', fail_ok=False, sort=False) 808 | 809 | run.run_command( 810 | ['php', '-m'], 'php_modules', fail_ok=False, sort=False) 811 | run.run_command( 812 | ['php', '-i'], 'php_info', fail_ok=False, sort=False) 813 | if os.path.exists('/usr/bin/pecl'): 814 | run.run_command( 815 | ['pecl', 'list'], 'pecl-list', fail_ok=True, sort=False) 816 | 817 | 818 | # copy custom files and directories 819 | for section in Config.sections(): 820 | try: 821 | # Optional options 822 | if Config.has_option(section, 'failok') and Config.get(section, 'failok').lower() == "true": 823 | failokcfg = True 824 | # If failok is anything except true... 825 | else: 826 | failokcfg = False 827 | 828 | cfgtype = Config.get(section, 'type').lower() 829 | if cfgtype == 'file': 830 | cpfile = Config.get(section, 'file') 831 | run.copy_file(cpfile, fail_ok=failokcfg) 832 | 833 | elif cfgtype == 'directory': 834 | cpdir = Config.get(section, 'directory') 835 | if Config.has_option(section, 'file_pattern'): 836 | config_file_pattern = Config.get(section, 'file_pattern') 837 | else: 838 | config_file_pattern = '.*' 839 | 840 | if not cpdir.endswith("/"): 841 | cpdir = cpdir + '/' 842 | run.copy_dir(cpdir, file_pattern=config_file_pattern, 843 | fail_ok=failokcfg, sort=False) 844 | 845 | except configparser.NoOptionError as e: 846 | report_error( 847 | "Check config file options for section %s: %s" % (section, e)) 848 | except Exception as e: 849 | report_error( 850 | "Could not parse config file section %s: %s" % (section, e)) 851 | 852 | report_info("\n") 853 | 854 | if ('post' in phase or 'rollback' in phase or options.force_compare) and not options.silent_enabled: 855 | compare_files(options) 856 | 857 | 858 | if options.archive_enabled: 859 | archive_location, success = create_archive( 860 | tagdir, workdir, options.tag, options.overwrite_enabled) 861 | if success: 862 | report_info("Archive saved to %s" % archive_location) 863 | 864 | report_info("Finished! Backups were saved to %s/*.%s" % (workdir, phase)) 865 | -------------------------------------------------------------------------------- /configsnap.help2man: -------------------------------------------------------------------------------- 1 | [Files] 2 | \fI/etc/configsnap/additional.conf\fR 3 | .RS 4 4 | Optional configuration file to define additional command output and files to collect. 5 | .RE 6 | 7 | .PP 8 | Commands are all run as root, so the custom collection configuration file must 9 | be owned by root and not read or writable by other users. The file format is: 10 | 11 | .PP 12 | All custom file collections in the additional.conf file must begin with a "[section]" which contains the following options. 13 | 14 | .PP 15 | \fIType=\fR 16 | .RS 4 17 | Can take values \fBCommand\fR, \fBFile\fR, or \fBDirectory\fR depending on the action that needs to be performed. 18 | 19 | .RS 4 20 | If \fBType=Command\fR then the output from the specified command will be save to a file named after the section. The command must be specified using the full path to the executable. e.g. /bin/ss -tanp. \fBCommand\fR supports several additional configuration options. 21 | 22 | If \fBType=File\fR then the specified file will be save to the backup directory with suffix matching the stage. The full path to the file must be used. \fBType=File\fR supports the same \fBFailOk\fR and \fBCompare\fR options as \fBCommand\fR. 23 | 24 | If \fBType=Directory\fR then the contents of the directory will be a subfolder of the same name within the backup directory. The path to the directory should include a trailing '/'. \fBDirectory\fR supports the \fBFailOk\fR, \fBCompare\fR (see \fBType=Command\fR), and the following. 25 | .RE 26 | .RE 27 | .PP 28 | \fISort=\fR 29 | .RS 4 30 | Whether to sort the output of the command, (default: False) 31 | .RE 32 | .PP 33 | \fIFailOk=\fR 34 | .RS 4 35 | Report errors when configsnap runs this section, (default: False) 36 | .RE 37 | .PP 38 | \fICompare=\fR 39 | .RS 4 40 | Produce a diff between the pre/post files when running configsnap, (default: False) 41 | .RE 42 | .PP 43 | \fIFile_Pattern=\fR 44 | .RS 4 45 | Rather than saving every file in a directory, backup only ones matching the Python regex pattern, (default: .*). See \fBExamples\fR below. 46 | .RE 47 | .RE 48 | 49 | \fBExamples\fR 50 | 51 | # Recording the output of a command into a "psspecial." file containing the output. 52 | [psspecial] 53 | Type: command 54 | Command: /bin/ps -aux 55 | Compare: True 56 | 57 | # Recording an additional file, stored as "debconf." 58 | [debconf.conf] 59 | Type: file 60 | File: /etc/debconf.conf 61 | Failok: True 62 | 63 | # Recursively Recording all files from /etc/ssh/ directory, with sub-files appended with ".". 64 | [ssh] 65 | Type: directory 66 | Directory: /etc/ssh/ 67 | 68 | # Recording all files from /etc/fail2ban/ directory matching '.*\\.local$', with sub-files appended with "." 69 | [fail2ban] 70 | Type: directory 71 | Directory: /etc/fail2ban 72 | File_Pattern: .*\\.local$ 73 | -------------------------------------------------------------------------------- /configsnap.spec: -------------------------------------------------------------------------------- 1 | Name: configsnap 2 | Version: 0.21.1 3 | Release: 1%{?dist} 4 | Summary: Record and compare system state 5 | License: ASL 2.0 6 | URL: https://github.com/rackerlabs/%{name} 7 | Source0: https://github.com/rackerlabs/%{name}/archive/%{version}.tar.gz 8 | BuildArch: noarch 9 | BuildRequires: help2man 10 | %if 0%{?rhel} >= 8 || 0%{?fedora} 11 | BuildRequires: python3 12 | %else 13 | BuildRequires: python2 14 | %endif 15 | 16 | %description 17 | configsnap records important system state information and can optionally compare 18 | with a previous state and identify changes 19 | 20 | %prep 21 | %setup -q 22 | 23 | %build 24 | %if 0%{?rhel} >= 8 || 0%{?fedora} 25 | sed -i 's#/bin/python$#/bin/python3#g' ./%{name} 26 | %endif 27 | help2man --include=%{name}.help2man --no-info ./%{name} -o %{name}.man 28 | 29 | 30 | %install 31 | mkdir -p %{buildroot}%{_sbindir} \ 32 | %{buildroot}%{_mandir}/man1 \ 33 | %{buildroot}%{_sysconfdir}/%{name} 34 | install -p -m 0755 %{name} %{buildroot}%{_sbindir} 35 | install -p -m 0644 %{name}.man %{buildroot}%{_mandir}/man1/%{name}.1 36 | install -p -m 0600 additional.conf %{buildroot}%{_sysconfdir}/%{name}/additional.conf 37 | 38 | %files 39 | %{!?_licensedir:%global license %doc} 40 | %license LICENSE 41 | %doc README.md 42 | %doc NEWS 43 | %doc MAINTAINERS.md 44 | %config(noreplace) %{_sysconfdir}/%{name}/additional.conf 45 | %{_mandir}/man1/%{name}.1* 46 | %{_sbindir}/%{name} 47 | %{_sysconfdir}/%{name} 48 | 49 | %changelog 50 | * Mon Nov 20 2023 Mark Hyde - 0.21.1-1 51 | - Add /proc/cpuinfo (PR 128) 52 | 53 | * Wed May 12 2021 Christos Triantafyllidis - 0.20.1-1 54 | - Update python binary for python3 based distros 55 | 56 | * Fri May 07 2021 Nick Rhodes - 0.20.0-3 57 | - Fix build issues in Koji 58 | 59 | * Fri May 07 2021 Nick Rhodes - 0.20.0-1 60 | - Port to python3 compatibility (PR 120) 61 | 62 | * Sun Aug 16 2020 Nick Rhodes - 0.19.0-1 63 | - Added lsblk and blkid (PR 115) 64 | - Fix flake8 warnings (PR 118) 65 | - Filtering out update line in pcs status (PR 112) 66 | 67 | * Mon Feb 03 2020 Nick Rhodes - 0.18.0-1 68 | - Improvements to get_diff (PR 110) 69 | 70 | * Wed Jul 03 2019 Nick Rhodes - 0.17.1-1 71 | - Convert relative basedir to absolute path (PR 103) 72 | 73 | * Sun Jun 16 2019 Nick Rhodes - 0.17.0-1 74 | - Update diff function to use Popen.communicate() (PR 101) 75 | 76 | * Thu Jan 31 2019 Fedora Release Engineering - 0.16.2-2 77 | - Rebuilt for https://fedoraproject.org/wiki/Fedora_30_Mass_Rebuild 78 | 79 | * Sun Dec 02 2018 Nick Rhodes - 0.16.2-1 80 | - Only report skipping default additional.conf file when using custom file 81 | 82 | * Sun Nov 04 2018 Nick Rhodes - 0.16.1-1 83 | - Revert previous --config release with argparse rewrite 84 | - Add --config option for specifying custom a configuration file using optparse 85 | - Filter the "ip address show" output to remove lines containing valid_lft XXsec preferred_lft XXsec 86 | 87 | * Wed Oct 17 2018 Nick Rhodes - 0.16-1 88 | - Add --config option for specifying custom a configuration file 89 | 90 | * Sat Sep 15 2018 Nick Rhodes - 0.15-1 91 | - Added copy_dir function to recursively backup and diff directories 92 | - Add ability to use copy_dir in additional.conf along with a file pattern match 93 | 94 | * Tue Jul 31 2018 Paolo Gigante - 0.14-1 95 | - Adjusted -w option to only overwrite specific tagged files 96 | - Add option to compare existing files without gathering new data using the -C/--compare-only option 97 | - Added the option to capture post data and compare to phases other than *.pre using the --pre option 98 | - Added option to force a compare even id the phase does not contain "post" or "rollback" using the --force-compare option 99 | 100 | * Thu Jul 12 2018 Fedora Release Engineering - 0.13-3 101 | - Rebuilt for https://fedoraproject.org/wiki/Fedora_29_Mass_Rebuild 102 | 103 | * Wed Feb 07 2018 Fedora Release Engineering - 0.13-2 104 | - Rebuilt for https://fedoraproject.org/wiki/Fedora_28_Mass_Rebuild 105 | 106 | * Thu Aug 17 2017 Piers Cornwell 0.13-1 107 | - New option -a to create a tar archive of the output 108 | - New option -w to overwrite existing output 109 | - PEP8 fixes 110 | - Modify check for PHP presence 111 | 112 | * Wed Jul 26 2017 Fedora Release Engineering - 0.12-3 113 | - Rebuilt for https://fedoraproject.org/wiki/Fedora_27_Mass_Rebuild 114 | 115 | * Mon Jun 12 2017 Piers Cornwell 0.12-2 116 | - Record Pacemaker status 117 | - Don't raise exception if command doesn't exist 118 | - Add alternative path for lspci 119 | - Allow MySQL show databases to fail 120 | - Record PHP state 121 | - Record iptables rules 122 | - Documented tested platforms 123 | - Optional custom collection 124 | 125 | * Fri Feb 10 2017 Fedora Release Engineering - 0.11-3 126 | - Rebuilt for https://fedoraproject.org/wiki/Fedora_26_Mass_Rebuild 127 | 128 | * Wed Jan 25 2017 Christos Triantafyllidis 0.11-2 129 | - Updated spec according to Fedora Guidelines 130 | 131 | * Wed Dec 21 2016 Piers Cornwell 0.11-1 132 | - Renamed from getData to configsnap 133 | - Backup grubenv for grub2 134 | - Support for Fedora 135 | - Added man page 136 | - Record dm-multipath information 137 | - Continue if lvm isn't present 138 | - Allow PowerPath to be present, but with no LUNs 139 | 140 | * Wed Jul 27 2016 Piers Cornwell 0.10-1 141 | - Initial public release, version 0.10 142 | 143 | * Mon May 9 2016 Piers Cornwell 0.9-1 144 | - Initial standalone tagged release, version 0.9 145 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/configsnap.install: -------------------------------------------------------------------------------- 1 | configsnap /usr/sbin 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: configsnap 2 | Section: admin 3 | Maintainer: Nick Rhodes 4 | Priority: optional 5 | Standards-Version: 3.9.6 6 | Build-Depends: debhelper (>= 9) 7 | Homepage: https://github.com/rackerlabs/configsnap 8 | 9 | Package: configsnap 10 | Depends: 11 | python3, diffutils 12 | Architecture: all 13 | Description: configsnap records important system state information and can 14 | optionally compare with a previous state and identify changes 15 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ 5 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | fedora: &rhelconfig 4 | build: 5 | context: . 6 | dockerfile: ./dockerfiles/Dockerfile-fedora 7 | volumes: 8 | - .:/home/builduser/configsnap 9 | - ./rpmbuild/RPMS:/home/builduser/rpmbuild/RPMS 10 | - ./rpmbuild/SRPMS:/home/builduser/rpmbuild/SRPMS 11 | - ./rpmbuild/SOURCES:/home/builduser/rpmbuild/SOURCES 12 | el8: 13 | <<: *rhelconfig 14 | build: 15 | context: . 16 | dockerfile: ./dockerfiles/Dockerfile-el8 17 | el7: 18 | <<: *rhelconfig 19 | build: 20 | context: . 21 | dockerfile: ./dockerfiles/Dockerfile-el7 22 | el6: 23 | <<: *rhelconfig 24 | build: 25 | context: . 26 | dockerfile: ./dockerfiles/Dockerfile-el6 27 | buster: 28 | build: 29 | context: . 30 | dockerfile: ./dockerfiles/Dockerfile-buster 31 | volumes: 32 | - .:/home/builduser/configsnap 33 | -------------------------------------------------------------------------------- /dockerfiles/Dockerfile-buster: -------------------------------------------------------------------------------- 1 | FROM debian:buster 2 | RUN apt update \ 3 | && apt install -y python devscripts build-essential gawk help2man lsb-release \ 4 | && groupadd -g 1004 builduser \ 5 | && useradd -m -u 1003 -g builduser builduser 6 | 7 | USER builduser 8 | RUN mkdir /home/builduser/configsnap 9 | WORKDIR /home/builduser/configsnap 10 | CMD ["make","deb"] 11 | -------------------------------------------------------------------------------- /dockerfiles/Dockerfile-el6: -------------------------------------------------------------------------------- 1 | FROM centos:6 2 | RUN sed -ri -e 's/mirrorlist/#mirrorlist/' -e 's;#baseurl=http://mirror.centos.org/centos/\$releasever/;baseurl=http://vault.centos.org/6.10/;' /etc/yum.repos.d/CentOS-Base.repo 3 | RUN yum install -q -y git rpm-build rpm-devel rpmlint make python rpmdevtools redhat-lsb-core \ 4 | help2man \ 5 | && groupadd -g 1004 builduser \ 6 | && useradd -m -u 1003 -g builduser builduser 7 | 8 | USER builduser 9 | RUN mkdir /home/builduser/configsnap \ 10 | && rpmdev-setuptree 11 | WORKDIR /home/builduser/configsnap 12 | CMD ["make","rpm"] 13 | -------------------------------------------------------------------------------- /dockerfiles/Dockerfile-el7: -------------------------------------------------------------------------------- 1 | FROM centos:7 2 | RUN yum install -q -y git rpm-build rpm-devel rpmlint make python rpmdevtools help2man \ 3 | redhat-lsb-core \ 4 | && groupadd -g 1004 builduser \ 5 | && useradd -m -u 1003 -g builduser builduser 6 | 7 | USER builduser 8 | RUN mkdir /home/builduser/configsnap \ 9 | && rpmdev-setuptree 10 | WORKDIR /home/builduser/configsnap 11 | CMD ["make","rpm"] 12 | -------------------------------------------------------------------------------- /dockerfiles/Dockerfile-el8: -------------------------------------------------------------------------------- 1 | FROM centos:8 2 | RUN yum install -y epel-release && yum install --enablerepo powertools -q -y git \ 3 | rpm-build rpm-devel rpmlint epel-rpm-macros make python3 python3-devel \ 4 | rpmdevtools help2man redhat-lsb-core \ 5 | && groupadd -g 1004 builduser \ 6 | && useradd -m -u 1003 -g builduser builduser 7 | 8 | USER builduser 9 | RUN mkdir /home/builduser/configsnap \ 10 | && rpmdev-setuptree 11 | WORKDIR /home/builduser/configsnap 12 | CMD ["make","rpm"] 13 | -------------------------------------------------------------------------------- /dockerfiles/Dockerfile-fedora: -------------------------------------------------------------------------------- 1 | FROM fedora:latest 2 | RUN dnf install -y git rpm-build rpm-devel rpmlint make python2 rpmdevtools \ 3 | help2man python2-devel redhat-lsb-core \ 4 | && groupadd -g 1004 builduser \ 5 | && useradd -m -u 1003 -g builduser builduser 6 | 7 | USER builduser 8 | RUN mkdir /home/builduser/configsnap \ 9 | && rpmdev-setuptree 10 | WORKDIR /home/builduser/configsnap 11 | CMD ["make","rpm"] 12 | -------------------------------------------------------------------------------- /test_configsnap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2016-2021 Rackspace, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use 6 | # this file except in compliance with the License. You may obtain a copy of the 7 | # License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software distributed 12 | # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 13 | # CONDITIONS OF ANY KIND, either express or implied. See the License for the 14 | # specific language governing permissions and limitations under the License. 15 | 16 | 17 | # Functional tests 18 | from __future__ import print_function 19 | import inspect 20 | import os 21 | import re 22 | import subprocess 23 | import sys 24 | import shutil 25 | 26 | 27 | class TestResult: 28 | 29 | def __init__(self, stdout, stderr, retcode): 30 | # Remove ANSI color escape sequences from text 31 | ansi_escape = re.compile(r'\x1b[^m]*m') 32 | self.stdout = ansi_escape.sub('', stdout.decode('utf-8')) 33 | self.stderr = ansi_escape.sub('', stderr.decode('utf-8')) 34 | self.retcode = retcode 35 | 36 | def stdout(self): 37 | print(self.stdout) 38 | 39 | def stderr(self): 40 | print(self.stderr) 41 | 42 | def retcode(self): 43 | print(self.retcode) 44 | 45 | 46 | class FunctionalTests: 47 | 48 | cwd = os.path.dirname(os.path.realpath(__file__)) 49 | failurecount = 0 50 | 51 | def whoami(self): 52 | """Return the calling function's name""" 53 | return inspect.stack()[1][3] 54 | 55 | def failtest(self, test, text): 56 | print("%s FAIL %s" % (test, text)) 57 | self.failurecount += 1 58 | 59 | def run_command(self, command): 60 | """Run a command and return output and exit code 61 | 62 | Args: 63 | param1 (str): the command to run 64 | 65 | Returns: 66 | list: stdout, stderr, exitcode 67 | """ 68 | 69 | command_proc = subprocess.Popen( 70 | command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, cwd=self.cwd) 71 | output = command_proc.stdout.read() 72 | error = command_proc.stderr.read() 73 | returncode = command_proc.wait() 74 | 75 | return TestResult(output, error, returncode) 76 | 77 | def func1_customdir(self): 78 | """Customised output directory; -d commandline option""" 79 | test = self.whoami() 80 | o = self.run_command('./configsnap -d /tmp/test -t functests') 81 | if o.retcode != 0: 82 | self.failtest(test, "Exit code non-zero") 83 | else: 84 | print("%s PASS Exit code zero" % test) 85 | 86 | # Check that custom dir was created and has content 87 | if not os.path.isdir('/tmp/test'): 88 | self.failtest(test, "Custom dir doesn't exist") 89 | else: 90 | print("%s PASS Custom dir exists" % test) 91 | 92 | if len(os.listdir('/tmp/test')) < 1: 93 | self.failtest(test, "No files in custom dir") 94 | else: 95 | print("%s PASS Files in custom dir" % test) 96 | 97 | def func2_customtag(self): 98 | """Customised tag; -t command line option""" 99 | test = self.whoami() 100 | o = self.run_command('./configsnap -t randomalternativetag') 101 | if o.retcode != 0: 102 | self.failtest(test, "Exit code non-zero") 103 | else: 104 | print("%s PASS Exit code zero" % test) 105 | 106 | # Check tag name on dir 107 | if os.path.exists('/root/randomalternativetag'): 108 | print("%s PASS alternative tag dir exists" % test) 109 | else: 110 | self.failtest(test, "Alternative tag dir doesn't exist") 111 | 112 | if len(os.listdir('/root/randomalternativetag/configsnap')) < 1: 113 | self.failtest(test, "No files in collection dir") 114 | else: 115 | print("%s PASS Files in collection dir" % test) 116 | 117 | def func3_overwrite(self): 118 | """Overwrite workdir; -w command line option""" 119 | test = self.whoami() 120 | for i in range(1, 4): 121 | o = self.run_command('./configsnap -t overwrite -p pre -w') 122 | if o.retcode != 0: 123 | self.failtest(test, "Exit code non-zero, run %i" % i) 124 | else: 125 | print("%s PASS Exit code zero, run %i" % (test, i)) 126 | 127 | def func4_error_handling_nooverwrite(self): 128 | """Don't overwrite by default""" 129 | test = self.whoami() 130 | o = self.run_command('./configsnap -t nooverwrite -p pre') 131 | if o.retcode != 0: 132 | self.failtest(test, "Exit code non-zero, initial run") 133 | else: 134 | print("%s PASS Exit code zero, initial run" % test) 135 | 136 | o = self.run_command('./configsnap -t nooverwrite -p pre') 137 | if o.retcode != 1: 138 | print(o.retcode) 139 | self.failtest(test, "Exit code not 1, second run") 140 | else: 141 | print("%s PASS Exit code 1, didn't overwrite, second run" % test) 142 | 143 | 144 | def main(): 145 | """Clean root directory test files""" 146 | for test_dir in ["nooverwrite", "overwrite", "randomalternativetag"]: 147 | if os.path.isdir(os.path.join("/root/", test_dir)): 148 | print("deleting /root/" + test_dir) 149 | shutil.rmtree(os.path.join("/root/", test_dir)) 150 | 151 | f = FunctionalTests() 152 | functions = dir(f) 153 | for function in functions: 154 | if function.startswith('func'): 155 | getattr(f, function)() 156 | 157 | print("Tests complete; failures: %s" % f.failurecount) 158 | 159 | if f.failurecount != 0: 160 | sys.exit(1) 161 | 162 | if __name__ == "__main__": 163 | main() 164 | --------------------------------------------------------------------------------