diff --git a/library/scan_files.py b/library/scan_files.py new file mode 100644 index 0000000..3ab0929 --- /dev/null +++ b/library/scan_files.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python + +import os +import stat +from ansible.module_utils.basic import * # noqa + +DOCUMENTATION = ''' +--- +module: scan_files +short_description: Return file state information as fact data for a directory tree +description: + - Return file state information recursively for a directory tree on the filesystem +version_added: "1.9" +options: + path: + description: The path containing files to be analyzed + required: true + default: null + recursive: + description: scan this directory and all subdirectories + required: false + default: no + get_checksum: + description: Checksum files that you can access + required: false + default: false +requirements: [ ] +author: Matthew Jones +''' + +EXAMPLES = ''' +# Example fact output: +# host | success >> { +# "ansible_facts": { +# "files": [ +# { +# "atime": 1427313854.0755742, +# "checksum": "cf7566e6149ad9af91e7589e0ea096a08de9c1e5", +# "ctime": 1427129299.22948, +# "dev": 51713, +# "gid": 0, +# "inode": 149601, +# "isblk": false, +# "ischr": false, +# "isdir": false, +# "isfifo": false, +# "isgid": false, +# "islnk": false, +# "isreg": true, +# "issock": false, +# "isuid": false, +# "mode": "0644", +# "mtime": 1427112663.0321455, +# "nlink": 1, +# "path": "/var/log/dmesg.1.gz", +# "rgrp": true, +# "roth": true, +# "rusr": true, +# "size": 28, +# "uid": 0, +# "wgrp": false, +# "woth": false, +# "wusr": true, +# "xgrp": false, +# "xoth": false, +# "xusr": false +# }, +# { +# "atime": 1427314385.1155744, +# "checksum": "16fac7be61a6e4591a33ef4b729c5c3302307523", +# "ctime": 1427384148.5755742, +# "dev": 51713, +# "gid": 43, +# "inode": 149564, +# "isblk": false, +# "ischr": false, +# "isdir": false, +# "isfifo": false, +# "isgid": false, +# "islnk": false, +# "isreg": true, +# "issock": false, +# "isuid": false, +# "mode": "0664", +# "mtime": 1427384148.5755742, +# "nlink": 1, +# "path": "/var/log/wtmp", +# "rgrp": true, +# "roth": true, +# "rusr": true, +# "size": 48768, +# "uid": 0, +# "wgrp": true, +# "woth": false, +# "wusr": true, +# "xgrp": false, +# "xoth": false, +# "xusr": false +# }, +''' + + +def main(): + module = AnsibleModule( + argument_spec = dict(paths=dict(required=True, type='list'), + recursive=dict(required=False, default='no', type='bool'), + get_checksum=dict(required=False, default='no', type='bool'))) + files = [] + paths = module.params.get('paths') + for path in paths: + path = os.path.expanduser(path) + if not os.path.exists(path) or not os.path.isdir(path): + module.fail_json(msg = "Given path must exist and be a directory") + + get_checksum = module.params.get('get_checksum') + should_recurse = module.params.get('recursive') + if not should_recurse: + path_list = [os.path.join(path, subpath) for subpath in os.listdir(path)] + else: + path_list = [os.path.join(w_path, f) for w_path, w_names, w_file in os.walk(path) for f in w_file] + for filepath in path_list: + try: + st = os.stat(filepath) + except OSError: + continue + + mode = st.st_mode + d = { + 'path' : filepath, + 'mode' : "%04o" % stat.S_IMODE(mode), + 'isdir' : stat.S_ISDIR(mode), + 'ischr' : stat.S_ISCHR(mode), + 'isblk' : stat.S_ISBLK(mode), + 'isreg' : stat.S_ISREG(mode), + 'isfifo' : stat.S_ISFIFO(mode), + 'islnk' : stat.S_ISLNK(mode), + 'issock' : stat.S_ISSOCK(mode), + 'uid' : st.st_uid, + 'gid' : st.st_gid, + 'size' : st.st_size, + 'inode' : st.st_ino, + 'dev' : st.st_dev, + 'nlink' : st.st_nlink, + 'atime' : st.st_atime, + 'mtime' : st.st_mtime, + 'ctime' : st.st_ctime, + 'wusr' : bool(mode & stat.S_IWUSR), + 'rusr' : bool(mode & stat.S_IRUSR), + 'xusr' : bool(mode & stat.S_IXUSR), + 'wgrp' : bool(mode & stat.S_IWGRP), + 'rgrp' : bool(mode & stat.S_IRGRP), + 'xgrp' : bool(mode & stat.S_IXGRP), + 'woth' : bool(mode & stat.S_IWOTH), + 'roth' : bool(mode & stat.S_IROTH), + 'xoth' : bool(mode & stat.S_IXOTH), + 'isuid' : bool(mode & stat.S_ISUID), + 'isgid' : bool(mode & stat.S_ISGID), + } + if get_checksum and stat.S_ISREG(mode) and os.access(filepath, os.R_OK): + d['checksum'] = module.sha1(filepath) + files.append(d) + results = dict(ansible_facts=dict(files=files)) + module.exit_json(**results) + + +main() diff --git a/library/scan_insights.py b/library/scan_insights.py new file mode 100755 index 0000000..2e759a2 --- /dev/null +++ b/library/scan_insights.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python + +from ansible.module_utils.basic import * # noqa + +DOCUMENTATION = ''' +--- +module: scan_insights +short_description: Return insights id as fact data +description: + - Inspects the /etc/redhat-access-insights/machine-id file for insights id and returns the found id as fact data +version_added: "2.3" +options: +requirements: [ ] +author: Chris Meyers +''' + +EXAMPLES = ''' +# Example fact output: +# host | success >> { +# "ansible_facts": { +# "insights": { +# "system_id": "4da7d1f8-14f3-4cdc-acd5-a3465a41f25d" +# }, ... } +''' + + +INSIGHTS_SYSTEM_ID_FILE='/etc/redhat-access-insights/machine-id' + + +def get_system_id(filname): + system_id = None + try: + f = open(INSIGHTS_SYSTEM_ID_FILE, "r") + except IOError: + return None + else: + try: + data = f.readline() + system_id = str(data) + except (IOError, ValueError): + pass + finally: + f.close() + return system_id + + +def main(): + module = AnsibleModule( + argument_spec = dict() + ) + + system_id = get_system_id(INSIGHTS_SYSTEM_ID_FILE) + + results = { + 'ansible_facts': { + 'insights': { + 'system_id': system_id + } + } + } + module.exit_json(**results) + + +main() diff --git a/library/scan_packages.py b/library/scan_packages.py new file mode 100755 index 0000000..3fd2edc --- /dev/null +++ b/library/scan_packages.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python + +from ansible.module_utils.basic import * # noqa + +DOCUMENTATION = ''' +--- +module: scan_packages +short_description: Return installed packages information as fact data +description: + - Return information about installed packages as fact data +version_added: "1.9" +options: +requirements: [ ] +author: Matthew Jones +''' + +EXAMPLES = ''' +# Example fact output: +# host | success >> { +# "ansible_facts": { +# "packages": { +# "libbz2-1.0": [ +# { +# "version": "1.0.6-5", +# "source": "apt", +# "arch": "amd64", +# "name": "libbz2-1.0" +# } +# ], +# "patch": [ +# { +# "version": "2.7.1-4ubuntu1", +# "source": "apt", +# "arch": "amd64", +# "name": "patch" +# } +# ], +# "gcc-4.8-base": [ +# { +# "version": "4.8.2-19ubuntu1", +# "source": "apt", +# "arch": "amd64", +# "name": "gcc-4.8-base" +# }, +# { +# "version": "4.9.2-19ubuntu1", +# "source": "apt", +# "arch": "amd64", +# "name": "gcc-4.8-base" +# } +# ] +# } +''' + + +def rpm_package_list(): + import rpm + trans_set = rpm.TransactionSet() + installed_packages = {} + for package in trans_set.dbMatch(): + package_details = dict(name=package[rpm.RPMTAG_NAME], + version=package[rpm.RPMTAG_VERSION], + release=package[rpm.RPMTAG_RELEASE], + epoch=package[rpm.RPMTAG_EPOCH], + arch=package[rpm.RPMTAG_ARCH], + source='rpm') + if package_details['name'] not in installed_packages: + installed_packages[package_details['name']] = [package_details] + else: + installed_packages[package_details['name']].append(package_details) + return installed_packages + + +def deb_package_list(): + import apt + apt_cache = apt.Cache() + installed_packages = {} + apt_installed_packages = [pk for pk in apt_cache.keys() if apt_cache[pk].is_installed] + for package in apt_installed_packages: + ac_pkg = apt_cache[package].installed + package_details = dict(name=package, + version=ac_pkg.version, + arch=ac_pkg.architecture, + source='apt') + if package_details['name'] not in installed_packages: + installed_packages[package_details['name']] = [package_details] + else: + installed_packages[package_details['name']].append(package_details) + return installed_packages + + +def main(): + module = AnsibleModule( + argument_spec = dict(os_family=dict(required=True)) + ) + ans_os = module.params['os_family'] + if ans_os in ('RedHat', 'Suse', 'openSUSE Leap'): + packages = rpm_package_list() + elif ans_os == 'Debian': + packages = deb_package_list() + else: + packages = None + + if packages is not None: + results = dict(ansible_facts=dict(packages=packages)) + else: + results = dict(skipped=True, msg="Unsupported Distribution") + module.exit_json(**results) + + +main() diff --git a/library/scan_services.py b/library/scan_services.py new file mode 100644 index 0000000..7b331a4 --- /dev/null +++ b/library/scan_services.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python + +import re +from ansible.module_utils.basic import * # noqa + +DOCUMENTATION = ''' +--- +module: scan_services +short_description: Return service state information as fact data +description: + - Return service state information as fact data for various service management utilities +version_added: "1.9" +options: +requirements: [ ] +author: Matthew Jones +''' + +EXAMPLES = ''' +- monit: scan_services +# Example fact output: +# host | success >> { +# "ansible_facts": { +# "services": { +# "network": { +# "source": "sysv", +# "state": "running", +# "name": "network" +# }, +# "arp-ethers.service": { +# "source": "systemd", +# "state": "stopped", +# "name": "arp-ethers.service" +# } +# } +# } +''' + + +class BaseService(object): + + def __init__(self, module): + self.module = module + self.incomplete_warning = False + + +class ServiceScanService(BaseService): + + def gather_services(self): + services = {} + service_path = self.module.get_bin_path("service") + if service_path is None: + return None + initctl_path = self.module.get_bin_path("initctl") + chkconfig_path = self.module.get_bin_path("chkconfig") + + # sysvinit + if service_path is not None and chkconfig_path is None: + rc, stdout, stderr = self.module.run_command("%s --status-all 2>&1 | grep -E \"\\[ (\\+|\\-) \\]\"" % service_path, use_unsafe_shell=True) + for line in stdout.split("\n"): + line_data = line.split() + if len(line_data) < 4: + continue # Skipping because we expected more data + service_name = " ".join(line_data[3:]) + if line_data[1] == "+": + service_state = "running" + else: + service_state = "stopped" + services[service_name] = {"name": service_name, "state": service_state, "source": "sysv"} + + # Upstart + if initctl_path is not None and chkconfig_path is None: + p = re.compile('^\s?(?P.*)\s(?P\w+)\/(?P\w+)(\,\sprocess\s(?P[0-9]+))?\s*$') + rc, stdout, stderr = self.module.run_command("%s list" % initctl_path) + real_stdout = stdout.replace("\r","") + for line in real_stdout.split("\n"): + m = p.match(line) + if not m: + continue + service_name = m.group('name') + service_goal = m.group('goal') + service_state = m.group('state') + if m.group('pid'): + pid = m.group('pid') + else: + pid = None # NOQA + payload = {"name": service_name, "state": service_state, "goal": service_goal, "source": "upstart"} + services[service_name] = payload + + # RH sysvinit + elif chkconfig_path is not None: + #print '%s --status-all | grep -E "is (running|stopped)"' % service_path + p = re.compile( + '(?P.*?)\s+[0-9]:(?Pon|off)\s+[0-9]:(?Pon|off)\s+[0-9]:(?Pon|off)\s+' + '[0-9]:(?Pon|off)\s+[0-9]:(?Pon|off)\s+[0-9]:(?Pon|off)\s+[0-9]:(?Pon|off)') + rc, stdout, stderr = self.module.run_command('%s' % chkconfig_path, use_unsafe_shell=True) + # Check for special cases where stdout does not fit pattern + match_any = False + for line in stdout.split('\n'): + if p.match(line): + match_any = True + if not match_any: + p_simple = re.compile('(?P.*?)\s+(?Pon|off)') + match_any = False + for line in stdout.split('\n'): + if p_simple.match(line): + match_any = True + if match_any: + # Try extra flags " -l --allservices" needed for SLES11 + rc, stdout, stderr = self.module.run_command('%s -l --allservices' % chkconfig_path, use_unsafe_shell=True) + elif '--list' in stderr: + # Extra flag needed for RHEL5 + rc, stdout, stderr = self.module.run_command('%s --list' % chkconfig_path, use_unsafe_shell=True) + for line in stdout.split('\n'): + m = p.match(line) + if m: + service_name = m.group('service') + service_state = 'stopped' + if m.group('rl3') == 'on': + rc, stdout, stderr = self.module.run_command('%s %s status' % (service_path, service_name), use_unsafe_shell=True) + service_state = rc + if rc in (0,): + service_state = 'running' + #elif rc in (1,3): + else: + if 'root' in stderr or 'permission' in stderr.lower() or 'not in sudoers' in stderr.lower(): + self.incomplete_warning = True + continue + else: + service_state = 'stopped' + service_data = {"name": service_name, "state": service_state, "source": "sysv"} + services[service_name] = service_data + return services + + +class SystemctlScanService(BaseService): + + def systemd_enabled(self): + # Check if init is the systemd command, using comm as cmdline could be symlink + try: + f = open('/proc/1/comm', 'r') + except IOError: + # If comm doesn't exist, old kernel, no systemd + return False + for line in f: + if 'systemd' in line: + return True + return False + + def gather_services(self): + services = {} + if not self.systemd_enabled(): + return None + systemctl_path = self.module.get_bin_path("systemctl", opt_dirs=["/usr/bin", "/usr/local/bin"]) + if systemctl_path is None: + return None + rc, stdout, stderr = self.module.run_command("%s list-unit-files --type=service | tail -n +2 | head -n -2" % systemctl_path, use_unsafe_shell=True) + for line in stdout.split("\n"): + line_data = line.split() + if len(line_data) != 2: + continue + if line_data[1] == "enabled": + state_val = "running" + else: + state_val = "stopped" + services[line_data[0]] = {"name": line_data[0], "state": state_val, "source": "systemd"} + return services + + +def main(): + module = AnsibleModule(argument_spec = dict()) + service_modules = (ServiceScanService, SystemctlScanService) + all_services = {} + incomplete_warning = False + for svc_module in service_modules: + svcmod = svc_module(module) + svc = svcmod.gather_services() + if svc is not None: + all_services.update(svc) + if svcmod.incomplete_warning: + incomplete_warning = True + if len(all_services) == 0: + results = dict(skipped=True, msg="Failed to find any services. Sometimes this is due to insufficient privileges.") + else: + results = dict(ansible_facts=dict(services=all_services)) + if incomplete_warning: + results['msg'] = "WARNING: Could not find status for all services. Sometimes this is due to insufficient privileges." + module.exit_json(**results) + + +main() diff --git a/library/win_scan_files.ps1 b/library/win_scan_files.ps1 new file mode 100644 index 0000000..6d114df --- /dev/null +++ b/library/win_scan_files.ps1 @@ -0,0 +1,102 @@ +#!powershell +# This file is part of Ansible +# +# Ansible 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. +# +# Ansible 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 Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + +$params = Parse-Args $args $true; + +$paths = Get-Attr $params "paths" $FALSE; +If ($paths -eq $FALSE) +{ + Fail-Json (New-Object psobject) "missing required argument: paths"; +} + +$get_checksum = Get-Attr $params "get_checksum" $false | ConvertTo-Bool; +$recursive = Get-Attr $params "recursive" $false | ConvertTo-Bool; + +function Date_To_Timestamp($start_date, $end_date) +{ + If($start_date -and $end_date) + { + Write-Output (New-TimeSpan -Start $start_date -End $end_date).TotalSeconds + } +} + +$files = @() + +ForEach ($path In $paths) +{ + "Path: " + $path + ForEach ($file in Get-ChildItem $path -Recurse: $recursive) + { + "File: " + $file.FullName + $fileinfo = New-Object psobject + Set-Attr $fileinfo "path" $file.FullName + $info = Get-Item $file.FullName; + $iscontainer = Get-Attr $info "PSIsContainer" $null; + $length = Get-Attr $info "Length" $null; + $extension = Get-Attr $info "Extension" $null; + $attributes = Get-Attr $info "Attributes" ""; + If ($info) + { + $accesscontrol = $info.GetAccessControl(); + } + Else + { + $accesscontrol = $null; + } + $owner = Get-Attr $accesscontrol "Owner" $null; + $creationtime = Get-Attr $info "CreationTime" $null; + $lastaccesstime = Get-Attr $info "LastAccessTime" $null; + $lastwritetime = Get-Attr $info "LastWriteTime" $null; + + $epoch_date = Get-Date -Date "01/01/1970" + If ($iscontainer) + { + Set-Attr $fileinfo "isdir" $TRUE; + } + Else + { + Set-Attr $fileinfo "isdir" $FALSE; + Set-Attr $fileinfo "size" $length; + } + Set-Attr $fileinfo "extension" $extension; + Set-Attr $fileinfo "attributes" $attributes.ToString(); + # Set-Attr $fileinfo "owner" $getaccesscontrol.Owner; + # Set-Attr $fileinfo "owner" $info.GetAccessControl().Owner; + Set-Attr $fileinfo "owner" $owner; + Set-Attr $fileinfo "creationtime" (Date_To_Timestamp $epoch_date $creationtime); + Set-Attr $fileinfo "lastaccesstime" (Date_To_Timestamp $epoch_date $lastaccesstime); + Set-Attr $fileinfo "lastwritetime" (Date_To_Timestamp $epoch_date $lastwritetime); + + If (($get_checksum) -and -not $fileinfo.isdir) + { + $hash = Get-FileChecksum($file.FullName); + Set-Attr $fileinfo "checksum" $hash; + } + + $files += $fileinfo + } +} + +$result = New-Object psobject @{ + ansible_facts = New-Object psobject @{ + files = $files + } +} + +Exit-Json $result; diff --git a/library/win_scan_packages.ps1 b/library/win_scan_packages.ps1 new file mode 100644 index 0000000..2ab3fdb --- /dev/null +++ b/library/win_scan_packages.ps1 @@ -0,0 +1,66 @@ +#!powershell +# This file is part of Ansible +# +# Ansible 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. +# +# Ansible 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 Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + +$uninstall_native_path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" +$uninstall_wow6432_path = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" + +if ([System.IntPtr]::Size -eq 4) { + + # This is a 32-bit Windows system, so we only check for 32-bit programs, which will be + # at the native registry location. + + [PSObject []]$packages = Get-ChildItem -Path $uninstall_native_path | + Get-ItemProperty | + Select-Object -Property @{Name="name"; Expression={$_."DisplayName"}}, + @{Name="version"; Expression={$_."DisplayVersion"}}, + @{Name="publisher"; Expression={$_."Publisher"}}, + @{Name="arch"; Expression={ "Win32" }} | + Where-Object { $_.name } + +} else { + + # This is a 64-bit Windows system, so we check for 64-bit programs in the native + # registry location, and also for 32-bit programs under Wow6432Node. + + [PSObject []]$packages = Get-ChildItem -Path $uninstall_native_path | + Get-ItemProperty | + Select-Object -Property @{Name="name"; Expression={$_."DisplayName"}}, + @{Name="version"; Expression={$_."DisplayVersion"}}, + @{Name="publisher"; Expression={$_."Publisher"}}, + @{Name="arch"; Expression={ "Win64" }} | + Where-Object { $_.name } + + $packages += Get-ChildItem -Path $uninstall_wow6432_path | + Get-ItemProperty | + Select-Object -Property @{Name="name"; Expression={$_."DisplayName"}}, + @{Name="version"; Expression={$_."DisplayVersion"}}, + @{Name="publisher"; Expression={$_."Publisher"}}, + @{Name="arch"; Expression={ "Win32" }} | + Where-Object { $_.name } + +} + +$result = New-Object psobject @{ + ansible_facts = New-Object psobject @{ + packages = $packages + } + changed = $false +} + +Exit-Json $result; diff --git a/library/win_scan_services.ps1 b/library/win_scan_services.ps1 new file mode 100644 index 0000000..3de8ac4 --- /dev/null +++ b/library/win_scan_services.ps1 @@ -0,0 +1,30 @@ +#!powershell +# This file is part of Ansible +# +# Ansible 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. +# +# Ansible 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 Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + +$result = New-Object psobject @{ + ansible_facts = New-Object psobject @{ + services = Get-Service | + Select-Object -Property @{Name="name"; Expression={$_."DisplayName"}}, + @{Name="win_svc_name"; Expression={$_."Name"}}, + @{Name="state"; Expression={$_."Status".ToString().ToLower()}} + } + changed = $false +} + +Exit-Json $result;