├── .gitignore ├── CHANGES.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── README.txt ├── bin ├── pysa └── pysa2puppet ├── examples ├── .gitignore ├── filters.addition ├── filters.discard ├── filters.replace ├── filters.update └── pysa.cfg ├── pysa.py ├── pysa ├── __init__.py ├── config.py ├── dependencies.py ├── exception.py ├── filter │ ├── __init__.py │ ├── filter.py │ └── parser.py ├── madeira.py ├── output.py ├── preprocessing.py ├── puppet │ ├── __init__.py │ ├── build.py │ ├── converter.py │ └── objects.py ├── salt │ ├── __init__.py │ ├── build.py │ ├── converter.py │ └── objects.py ├── scanner │ ├── __init__.py │ ├── actions │ │ ├── __init__.py │ │ ├── base.py │ │ ├── cron.py │ │ ├── file.py │ │ ├── gem.py │ │ ├── group.py │ │ ├── host.py │ │ ├── mount.py │ │ ├── npm.py │ │ ├── package.py │ │ ├── php.py │ │ ├── process.py │ │ ├── pypi.py │ │ ├── repository.py │ │ ├── service.py │ │ ├── source.py │ │ ├── sshkey.py │ │ ├── user.py │ │ └── utils.py │ ├── object │ │ ├── __init__.py │ │ ├── cron.py │ │ ├── file.py │ │ ├── group.py │ │ ├── host.py │ │ ├── mount.py │ │ ├── object_base.py │ │ ├── package.py │ │ ├── process.py │ │ ├── repository.py │ │ ├── service.py │ │ ├── source.py │ │ ├── sshkey.py │ │ └── user.py │ └── scanner_handler.py └── tools.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VisualOps/pysa/a3493826f5feb3ba59bed227fa397ce1c7916e9d/.gitignore -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | v0.3bx, 2013.12.20 -- general fixes 2 | v0.3b, 2013.10.15 -- Beta1 (0.3) release 3 | v0.2.5a, 2013.7.24 -- SaltStack module improvements and bug fixes 4 | v0.2.4a, 2013.7.23 -- Introducing SaltStack module, typo improvement, bug fixes. 5 | v0.2.3ax, 2013.7.xx -- Bug fixes 6 | v0.2.3a, 2013.6.29 -- Improvment on puppet module (readability mostly) 7 | v0.2.2ax, 2013.6.13 -- Minor alpha fix are not written in docs anymore (0.2.2ax -> 0.2.2a) 8 | v0.2.2a1, 2013.6.13 -- Add manpage try (not functionnal) 9 | v0.2.2a, 2013.6.13 -- Config file addition + Blueprint comparison (doc) 10 | v0.2.1a4, 2013.6.11 -- Alpha release - Doc fix 11 | v0.2.1a3, 2013.6.11 -- Alpha release - ssh key fix 12 | v0.2.1a2, 2013.6.10 -- Alpha release - Doc fix 13 | v0.2.1a1, 2013.6.10 -- Alpha release - Doc fix 14 | v0.2.1a, 2013.6.09 -- Alpha release - Install issues fix - First installable version (please don't use any previous version). 15 | v0.2.0ax, 2013.6.09 -- Fixes and tests setup script. 16 | v0.2.0a, 2013.6.07 -- Alpha release. Includes dependencies improvment, pre-alpha fixes and replication helper. 17 | v0.1.xax, 2013.6.xx -- Pre-alpha fix, doc formating. 18 | v0.1a, 2013.5.24 -- Initial release. 19 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include examples/* 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Welcome to Pysa project page 2 | 3 | Pysa is a configuration reverse engineering software, which is aimed to help anyone who wants to replicate an existing computer configuration - and not simply clone the entire machine. It can be used to migrate configurations from one computer to another (including physical machines to virtual Clouds), backup existing configurations, or for any other migration purpose. 4 | 5 | ## Install 6 | 7 | Pysa has been designed for UNIX Operating Systems. However, only Linux based distributions are supported for now (tested on Ubuntu, Debian, Cent OS, RedHat and Amazon Linux latest and LTS versions). More will come later. 8 | 9 | There are two ways to install Pysa: using the sources files or using the PyPI packages repository. 10 | 11 | ### Using PyPI (recommended) 12 | 13 | - Ensure pip is setup on your computer (you can alternatively use other PyPI managers, like easy_install) 14 | - Setup Pysa in its latest version pip install pysa 15 | - Run Pysa (see documentation) 16 | 17 | ### Sources setup 18 | 19 | - Clone this git repo `git clone https://github.com/MadeiraCloud/pysa.git` 20 | - Run Pysa (pysa.py - see documentation) 21 | 22 | ## Documentation 23 | 24 | You can access the documentation at http://madeira-cloud-pysa-document.readthedocs.org/en/latest/ 25 | 26 | ## Authors and Contributors 27 | 28 | - Thibault BRONCHAIN thibault {at} mc2 {dot} io 29 | - Michael CHO michael {at} mc2 {dot} io 30 | - Ken CHEN ken {at} mc2 {dot} io 31 | 32 | ## Support or Contact 33 | 34 | Having trouble, remarks or suggestions about Pysa? Contact us at pysa@mc2.io and we'll help you sort it out. 35 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Welcome to Pysa project page 2 | ---------------------------- 3 | 4 | Pysa is a configuration reverse engineering software, which is aimed to 5 | help anyone who wants to replicate an existing computer configuration - 6 | and not simply clone the entire machine. It can be used to migrate 7 | configurations from one computer to another (including physical machines 8 | to virtual Clouds), backup existing configurations, or for any other 9 | migration purpose. 10 | 11 | Install 12 | ------- 13 | 14 | Pysa has been designed for UNIX Operating Systems. However, only Linux 15 | based distributions are supported for now (tested on Ubuntu, Debian, 16 | Cent OS, RedHat and Amazon Linux latest and LTS versions). More will 17 | come later. 18 | 19 | There are two ways to install Pysa: using the sources files or using the 20 | PyPI packages repository. 21 | 22 | Using PyPI (recommended) 23 | ~~~~~~~~~~~~~~~~~~~~~~~~ 24 | 25 | - Ensure pip is setup on your computer (you can alternatively use other 26 | PyPI managers, like easy\_install) 27 | - Setup Pysa in its latest version pip install pysa 28 | - Run Pysa (see documentation) 29 | 30 | Sources setup 31 | ~~~~~~~~~~~~~ 32 | 33 | - Clone this git repo 34 | ``git clone https://github.com/MadeiraCloud/pysa.git`` 35 | - Run Pysa (pysa.py - see documentation) 36 | 37 | Documentation 38 | ------------- 39 | 40 | You can access the documentation at 41 | http://madeira-cloud-pysa-document.readthedocs.org/en/latest/ 42 | 43 | Authors and Contributors 44 | ------------------------ 45 | 46 | - Thibault BRONCHAIN thibault {at} mc2 {dot} io 47 | - Michael CHO michael {at} mc2 {dot} io 48 | - Ken CHEN ken {at} mc2 {dot} io 49 | 50 | Support or Contact 51 | ------------------ 52 | 53 | Having trouble, remarks or suggestions about Pysa? Contact us at 54 | pysa@mc2.io and we'll help you sort it out. 55 | -------------------------------------------------------------------------------- /bin/pysa: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | ''' 3 | Main file 4 | 5 | pysa - reverse a complete computer setup 6 | Copyright (C) 2013 MadeiraCloud Ltd. 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | @author: Thibault BRONCHAIN 22 | ''' 23 | 24 | from optparse import * 25 | import logging 26 | import re 27 | 28 | from pysa.tools import Tools 29 | from pysa.preprocessing import Preprocessing 30 | from pysa.config import * 31 | from pysa.exception import * 32 | from pysa.madeira import * 33 | 34 | from pysa.filter.parser import FParser 35 | from pysa.scanner.scanner_handler import module_scan 36 | 37 | from pysa.puppet.converter import PuppetConverter 38 | from pysa.puppet.build import PuppetBuild 39 | 40 | from pysa.salt.converter import SaltConverter 41 | from pysa.salt.build import SaltBuild 42 | 43 | 44 | # global defines 45 | USAGE = 'usage: %prog [-hqps] [-m module_name] [-o output_path] [-c config_file_path] [-f filter_config_path] [-l {-u madeira_username}|{-i madeira_id}]' 46 | VERSION_NBR = '0.3b' 47 | VERSION = '%prog '+VERSION_NBR 48 | 49 | # logger settings 50 | LOG_FILENAME = '/tmp/scanner.log' 51 | LOG_FORMAT = '%(asctime)s-%(name)s-%(levelname)s-%(message)s' 52 | def __log(lvl): 53 | level = logging.getLevelName(lvl) 54 | formatter = logging.Formatter(LOG_FORMAT) 55 | handler = logging.StreamHandler() 56 | logger = logging.getLogger() 57 | handler.setFormatter(formatter) 58 | logger.setLevel(level) 59 | logger.addHandler(handler) 60 | 61 | 62 | # scanner class 63 | class Scanner(): 64 | def __init__(self, filters=None): 65 | self.resources = None 66 | self.filters = filters 67 | self.preprocessed = None 68 | 69 | @GeneralException 70 | # get resource from different modules 71 | def scan(self): 72 | logging.info('Scanner.scan(): start scanning') 73 | self.resources = module_scan(self.filters if self.filters else None) 74 | 75 | @GeneralException 76 | # generate puppet files 77 | def preprocessing(self, module): 78 | if not self.resources: 79 | logging.error('Scanner.preprocessing(): No resources') 80 | return 81 | logging.info('Scanner.preprocessing(): Running') 82 | return Preprocessing(module).run(self.resources) 83 | 84 | @GeneralException 85 | # generate puppet files 86 | def show_puppet(self, path, module): 87 | # if not self.preprocessed: 88 | # logging.error('Scanner.show_puppet(): No data') 89 | # return 90 | logging.info('Scanner.show_puppet(): Puppet files will be stored in path: %s' % path) 91 | puppetdict = PuppetConverter(self.preprocessing('pysa.puppet'), self.filters) 92 | p = puppetdict.run() 93 | puppet = PuppetBuild(p, path, module) 94 | puppet.run() 95 | 96 | @GeneralException 97 | # generate salt files 98 | def show_salt(self, path, module): 99 | # if not self.preprocessed: 100 | # logging.error('Scanner.show_salt(): No data') 101 | # return 102 | logging.info('Scanner.show_salt(): Salt files will be stored in path: %s' % path) 103 | saltdict = SaltConverter(self.preprocessing('pysa.salt'), self.filters) 104 | s = saltdict.run() 105 | salt = SaltBuild(s, path, module) 106 | salt.run() 107 | 108 | # print header 109 | def print_header(): 110 | print "Pysa v"+VERSION_NBR 111 | print ''' 112 | 113 | pysa - reverse a complete computer setup 114 | Copyright (C) 2013 MadeiraCloud Ltd. 115 | 116 | Thank you for using pysa! 117 | Be aware that you are using an early-build (alpha release). 118 | To provide the best result, ensure that you are not using an outdated version (check out http://github.com/MadeiraCloud/pysa or http://pypi.python.org/pypi/Pysa to get the latest version). 119 | Please don't hesitate to report any bugs, requirements, advice, criticisms, hate or love messages to either pysa-user@googlegroups.com for public discussions and pysa@mc2.io for private messages. 120 | ''' 121 | 122 | # option parser - user handler 123 | def check_user(option, opt_str, value, parser): 124 | if parser.values.user or parser.values.id: 125 | setattr(parser.values, option.dest, True) 126 | else: 127 | raise OptionValueError("can't use -l without -u or -i (see usage)") 128 | 129 | # option parser 130 | def main_parse(): 131 | parser = OptionParser(usage=USAGE, version=VERSION) 132 | parser.add_option("-c", "--config", action="store", dest="config", 133 | help="specify config file" 134 | ) 135 | parser.add_option("-p", "--puppet", action="store_true", dest="puppet", default=False, 136 | help="scan packages and generate the puppet manifests" 137 | ) 138 | parser.add_option("-s", "--salt", action="store_true", dest="salt", default=False, 139 | help="[EXPERIMENTAL] scan packages and generate the salt manifests" 140 | ) 141 | parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, 142 | help="operate quietly" 143 | ) 144 | parser.add_option("-m", "--module", action="store", dest="module", default="pysa", 145 | help="define module name" 146 | ) 147 | parser.add_option("-o", "--output", action="store", dest="output", default="./output", 148 | help="Path to output" 149 | ) 150 | parser.add_option("-f", "--filter", action="store", dest="filter", 151 | help="add some user filters" 152 | ) 153 | parser.add_option("-l", "--madeira", action="callback", callback=check_user, dest='l', 154 | help="post data to madeira" 155 | ) 156 | parser.add_option("-u", "--user", "--username", action="store", dest='user', 157 | help="madeira username" 158 | ) 159 | parser.add_option("-i", "--id", action="store", dest='id', 160 | help="identify user id" 161 | ) 162 | return parser.parse_args() 163 | 164 | def main(): 165 | # print header 166 | print_header() 167 | 168 | # options parsing 169 | options, args = main_parse() 170 | __log(('ERROR' if options.quiet else 'INFO')) 171 | output = (options.output if options.output else "./output") 172 | module = (options.module if options.module else "pysa") 173 | user = (options.user if options.user else None) 174 | uid = (options.id if options.id else None) 175 | 176 | # config parser 177 | Config(options.config if options.config else None) 178 | 179 | # filters parsing 180 | filter_parser = FParser(options.filter if options.filter else None) 181 | filters = filter_parser.run() 182 | 183 | # scan for files 184 | s = Scanner(filters) 185 | s.scan() 186 | # generate puppet output 187 | if options.puppet: 188 | s.show_puppet(output, module) 189 | # generate salt output 190 | if options.salt: 191 | s.show_salt(output, module) 192 | 193 | # save to madeira accound 194 | if options.l: 195 | m = Madeira(user, uid, output, module) 196 | m.send() 197 | 198 | 199 | if __name__ == '__main__': 200 | main() 201 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | -------------------------------------------------------------------------------- /examples/filters.addition: -------------------------------------------------------------------------------- 1 | # Part of pysa utility 2 | # 3 | # Filtering file example 4 | # Please see pysa man page for section description 5 | # 6 | 7 | [addition.source] 8 | # set global user/password for all sources 9 | # def: xxx = xxx 10 | ownner = root 11 | password = whatever 12 | 13 | [addition.source.name.myproject] 14 | # add devops source's password 15 | # def: xxx = xxx 16 | ownner = root 17 | password = secure-password 18 | 19 | [addition.source.scm.hg] 20 | # change the mode of all the hg source 21 | # def: xxx = xxx 22 | mode = 0744 23 | 24 | [addition.package.name.zip] 25 | # change package's architecture 26 | # def: xxx = xxx 27 | platform = i386 28 | 29 | [addition.user.name.imauser] 30 | # change user's shell 31 | # def: xxx = xxx 32 | shell = /bin/sh 33 | 34 | [addition.service.name.imaservice] 35 | # change service's boot status 36 | # def: xxx = xxx 37 | ensure = False 38 | 39 | [addition.file.path./etc/test.conf] 40 | # change file's path 41 | # def: xxx = xxx 42 | path = /etc/test_1.conf 43 | -------------------------------------------------------------------------------- /examples/filters.discard: -------------------------------------------------------------------------------- 1 | # Part of pysa utility 2 | # 3 | # Filtering file example 4 | # Please see pysa man page for section description 5 | # 6 | 7 | [discard] 8 | # use it carefully, some resources might depend on others 9 | _resources = package, cron, group, mount, repository, service, key, user, process, gem, npm, php, pypi 10 | 11 | # package 12 | # def: package.xxx = xxx 13 | package.name = subversion, mercurial 14 | package.platform = x86_64, amd64 15 | 16 | # file 17 | # def: file.xxx = xxx 18 | file.path = /etc/host.conf 19 | 20 | # cron 21 | # def: cron.xxx = xxx 22 | 23 | # group 24 | # def: group.xxx = xxx 25 | 26 | # mount 27 | # def: mount.xxx = xxx 28 | 29 | # repo 30 | # def: repository.xxx = xxx 31 | 32 | # service 33 | # def: service.xxx = xxx 34 | service.provider = upstart 35 | 36 | # key 37 | # def: key.xxx = xxx 38 | 39 | # user 40 | # def: user.xxx = xxx 41 | user.name = mc 42 | 43 | # process 44 | # def: process.xxx = xxx 45 | 46 | # source 47 | # def: source.xxx = xxx 48 | source.name = devops 49 | source.source = 211.98.26.6 50 | -------------------------------------------------------------------------------- /examples/filters.replace: -------------------------------------------------------------------------------- 1 | # Part of pysa utility 2 | # 3 | # Filtering file example 4 | # Please see pysa man page for section description 5 | # 6 | 7 | # global item replacment 8 | [replace] 9 | 10 | # set filtering mode 11 | # def: _replaceall = true/false 12 | # required 13 | _replaceall = true 14 | 15 | # list replacment values 16 | # def: new_value = old_value(s) 17 | 127.0.1.1 = 127.0.0.1 18 | myhost.com = localdomain 19 | 20 | 21 | # specific item replacment 22 | [replace.files.content] 23 | 24 | # set filtering mode 25 | # def: _replaceall = true/false 26 | # required 27 | _replaceall = false 28 | 29 | # list exception(s) (use object primary key, see man(8) pysa) 30 | # def: _except = key1, ... 31 | _except = /etc/foo.conf 32 | 33 | # list replacment values 34 | # def: new_value = old_value(s) 35 | 192.168.0.12 = 192.168.12.34 36 | myolddomain.com = mynewdomain.com 37 | toto = tata, fwfw 38 | 39 | [replace.repos.content] 40 | # refer to previous section (duplicate content) 41 | # def: _contentrefer = section 42 | _contentrefer = replace.files.content 43 | -------------------------------------------------------------------------------- /examples/filters.update: -------------------------------------------------------------------------------- 1 | # Part of pysa utility 2 | # 3 | # Filtering file example 4 | # Please see pysa man page for section description 5 | # 6 | 7 | [update] 8 | # set filtering mode 9 | # def: _update = true/false 10 | # required 11 | _update = false 12 | 13 | # list exception(s) (use package name, see man(8) pysa) 14 | # def: except = package1, ... 15 | except = selinux*, libipa_hbac-python, kernel-firmware, libsss_idmap, sssd-client, perf 16 | -------------------------------------------------------------------------------- /examples/pysa.cfg: -------------------------------------------------------------------------------- 1 | # Part of pysa utility 2 | # 3 | # Config file example 4 | # Please see pysa help for details 5 | # 6 | 7 | # files section 8 | [files] 9 | # set search path 10 | # def: path = xxx:xxx 11 | path = /etc:/root/.ssh 12 | 13 | # keys section 14 | [keys] 15 | # set search path 16 | # def: path = xxx:xxx 17 | path = /root/.ssh 18 | 19 | # hosts section 20 | [hosts] 21 | # set hosts file path 22 | # def: path = xxx 23 | path = /etc/hosts 24 | 25 | # package managers 26 | [managers] 27 | # define yum/apt packages for other managers 28 | # def: _autoadd = True/False 29 | _autoadd = True 30 | # def: manager_name = manager_package 31 | pear = php-pear 32 | pecl = php-pear 33 | pip = python-pip 34 | npm = npm 35 | gem = rubygems 36 | -------------------------------------------------------------------------------- /pysa.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | ''' 3 | Main file 4 | 5 | pysa - reverse a complete computer setup 6 | Copyright (C) 2013 MadeiraCloud Ltd. 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | @author: Thibault BRONCHAIN 22 | ''' 23 | 24 | from optparse import * 25 | import logging 26 | import re 27 | 28 | from pysa.tools import Tools 29 | from pysa.preprocessing import Preprocessing 30 | from pysa.config import * 31 | from pysa.exception import * 32 | from pysa.madeira import * 33 | 34 | from pysa.filter.parser import FParser 35 | from pysa.scanner.scanner_handler import module_scan 36 | 37 | from pysa.puppet.converter import PuppetConverter 38 | from pysa.puppet.build import PuppetBuild 39 | 40 | from pysa.salt.converter import SaltConverter 41 | from pysa.salt.build import SaltBuild 42 | 43 | 44 | # global defines 45 | USAGE = 'usage: %prog [-hqps] [-m module_name] [-o output_path] [-c config_file_path] [-f filter_config_path] [-l {-u madeira_username}|{-i madeira_id}]' 46 | VERSION_NBR = '0.3b' 47 | VERSION = '%prog '+VERSION_NBR 48 | 49 | # logger settings 50 | LOG_FILENAME = '/tmp/scanner.log' 51 | LOG_FORMAT = '%(asctime)s-%(name)s-%(levelname)s-%(message)s' 52 | def __log(lvl): 53 | level = logging.getLevelName(lvl) 54 | formatter = logging.Formatter(LOG_FORMAT) 55 | handler = logging.StreamHandler() 56 | logger = logging.getLogger() 57 | handler.setFormatter(formatter) 58 | logger.setLevel(level) 59 | logger.addHandler(handler) 60 | 61 | 62 | # scanner class 63 | class Scanner(): 64 | def __init__(self, filters=None): 65 | self.resources = None 66 | self.filters = filters 67 | self.preprocessed = None 68 | 69 | @GeneralException 70 | # get resource from different modules 71 | def scan(self): 72 | logging.info('Scanner.scan(): start scanning') 73 | self.resources = module_scan(self.filters if self.filters else None) 74 | 75 | @GeneralException 76 | # generate puppet files 77 | def preprocessing(self, module): 78 | if not self.resources: 79 | logging.error('Scanner.preprocessing(): No resources') 80 | return 81 | logging.info('Scanner.preprocessing(): Running') 82 | return Preprocessing(module).run(self.resources) 83 | 84 | @GeneralException 85 | # generate puppet files 86 | def show_puppet(self, path, module): 87 | # if not self.preprocessed: 88 | # logging.error('Scanner.show_puppet(): No data') 89 | # return 90 | logging.info('Scanner.show_puppet(): Puppet files will be stored in path: %s' % path) 91 | puppetdict = PuppetConverter(self.preprocessing('pysa.puppet'), self.filters) 92 | p = puppetdict.run() 93 | puppet = PuppetBuild(p, path, module) 94 | puppet.run() 95 | 96 | @GeneralException 97 | # generate salt files 98 | def show_salt(self, path, module): 99 | # if not self.preprocessed: 100 | # logging.error('Scanner.show_salt(): No data') 101 | # return 102 | logging.info('Scanner.show_salt(): Salt files will be stored in path: %s' % path) 103 | saltdict = SaltConverter(self.preprocessing('pysa.salt'), self.filters) 104 | s = saltdict.run() 105 | salt = SaltBuild(s, path, module) 106 | salt.run() 107 | 108 | # print header 109 | def print_header(): 110 | print "Pysa v"+VERSION_NBR 111 | print ''' 112 | 113 | pysa - reverse a complete computer setup 114 | Copyright (C) 2013 MadeiraCloud Ltd. 115 | 116 | Thank you for using pysa! 117 | Be aware that you are using an early-build (alpha release). 118 | To provide the best result, ensure that you are not using an outdated version (check out http://github.com/MadeiraCloud/pysa or http://pypi.python.org/pypi/Pysa to get the latest version). 119 | Please don't hesitate to report any bugs, requirements, advice, criticisms, hate or love messages to either pysa-user@googlegroups.com for public discussions and pysa@mc2.io for private messages. 120 | ''' 121 | 122 | # option parser - user handler 123 | def check_user(option, opt_str, value, parser): 124 | if parser.values.user or parser.values.id: 125 | setattr(parser.values, option.dest, True) 126 | else: 127 | raise OptionValueError("can't use -l without -u or -i (see usage)") 128 | 129 | # option parser 130 | def main_parse(): 131 | parser = OptionParser(usage=USAGE, version=VERSION) 132 | parser.add_option("-c", "--config", action="store", dest="config", 133 | help="specify config file" 134 | ) 135 | parser.add_option("-p", "--puppet", action="store_true", dest="puppet", default=False, 136 | help="scan packages and generate the puppet manifests" 137 | ) 138 | parser.add_option("-s", "--salt", action="store_true", dest="salt", default=False, 139 | help="[EXPERIMENTAL] scan packages and generate the salt manifests" 140 | ) 141 | parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, 142 | help="operate quietly" 143 | ) 144 | parser.add_option("-m", "--module", action="store", dest="module", default="pysa", 145 | help="define module name" 146 | ) 147 | parser.add_option("-o", "--output", action="store", dest="output", default="./output", 148 | help="Path to output" 149 | ) 150 | parser.add_option("-f", "--filter", action="store", dest="filter", 151 | help="add some user filters" 152 | ) 153 | parser.add_option("-l", "--madeira", action="callback", callback=check_user, dest='l', 154 | help="post data to madeira" 155 | ) 156 | parser.add_option("-u", "--user", "--username", action="store", dest='user', 157 | help="madeira username" 158 | ) 159 | parser.add_option("-i", "--id", action="store", dest='id', 160 | help="identify user id" 161 | ) 162 | return parser.parse_args() 163 | 164 | def main(): 165 | # print header 166 | print_header() 167 | 168 | # options parsing 169 | options, args = main_parse() 170 | __log(('ERROR' if options.quiet else 'INFO')) 171 | output = (options.output if options.output else "./output") 172 | module = (options.module if options.module else "pysa") 173 | user = (options.user if options.user else None) 174 | uid = (options.id if options.id else None) 175 | 176 | # config parser 177 | Config(options.config if options.config else None) 178 | 179 | # filters parsing 180 | filter_parser = FParser(options.filter if options.filter else None) 181 | filters = filter_parser.run() 182 | 183 | # scan for files 184 | s = Scanner(filters) 185 | s.scan() 186 | # generate puppet output 187 | if options.puppet: 188 | s.show_puppet(output, module) 189 | # generate salt output 190 | if options.salt: 191 | s.show_salt(output, module) 192 | 193 | # save to madeira accound 194 | if options.l: 195 | m = Madeira(user, uid, output, module) 196 | m.send() 197 | 198 | 199 | if __name__ == '__main__': 200 | main() 201 | -------------------------------------------------------------------------------- /pysa/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VisualOps/pysa/a3493826f5feb3ba59bed227fa397ce1c7916e9d/pysa/__init__.py -------------------------------------------------------------------------------- /pysa/config.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Global configuration 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Thibault BRONCHAIN 21 | ''' 22 | 23 | from ConfigParser import SafeConfigParser 24 | 25 | from pysa.tools import * 26 | from pysa.exception import * 27 | 28 | 29 | # define who is a file 30 | FILE_CLASS = ['keys', 'repos', 'files'] 31 | # order 32 | ORDER_LIST = [ 33 | 'hosts', 34 | 'mounts', 35 | 'groups', 36 | 'users', 37 | 'dirs', 38 | 'keys', 39 | 'repos', 40 | 'packages', 41 | 'files', 42 | 'crons', 43 | 'sources', 44 | 'services', 45 | ] 46 | # null objects (avoid 0) 47 | NULL = ['', {}, [], None] 48 | # build-ins 49 | VOID_EQ = '_' 50 | ACTION_ID = '_' 51 | MAIN_SECTION = '_' 52 | SINGLE_SEC = '__' 53 | 54 | # configuration class 55 | class Config(): 56 | 57 | # default values 58 | c = { 59 | 'files' : { 60 | 'path' : '/etc:/root/.ssh' 61 | }, 62 | 'keys' : { 63 | 'path' : 'root/.ssh' 64 | }, 65 | 'hosts' : { 66 | 'path' : '/etc/hosts' 67 | }, 68 | 'managers' : { 69 | '_autoadd' : True, 70 | 'pear' : 'php-pear', 71 | 'pecl' : 'php-pear', 72 | 'pip' : 'python-pip', 73 | 'npm' : 'npm', 74 | 'gem' : 'rubygems', 75 | }, 76 | } 77 | files_path = c['files']['path'] 78 | scan_host = c['hosts']['path'] 79 | key_path = c['keys']['path'] 80 | managers_eq = c['managers'] 81 | platform = None 82 | 83 | # edit default values if config file 84 | def __init__(self, path=None): 85 | if not path: return 86 | self.__filename = path 87 | self.__parse_config() 88 | 89 | # parse config file 90 | @GeneralException 91 | def __parse_config(self): 92 | parser = SafeConfigParser() 93 | parser.read(self.__filename) 94 | for name in parser.sections(): 95 | config.c.setdefault(name, {}) 96 | for key, value in parser.items(name): 97 | if value == "True" : value = True 98 | elif value == "False": value = False 99 | config.c[name][key] = value 100 | -------------------------------------------------------------------------------- /pysa/dependencies.py: -------------------------------------------------------------------------------- 1 | ''' 2 | List dependencies for all resources 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Thibault BRONCHAIN 21 | ''' 22 | 23 | 24 | import re 25 | import copy 26 | 27 | from pysa.tools import * 28 | from pysa.config import * 29 | from pysa.exception import * 30 | 31 | 32 | SCM_EQ = { 33 | 'git' : ['git', 'git-all'], 34 | 'svn' : ['subversion'], 35 | 'hg' : ['mercurial'], 36 | } 37 | 38 | SELF_ORDER_EQ = { 39 | 'dirs' : 'path', 40 | 'sources' : 'path', 41 | 'mounts' : 'name', 42 | } 43 | 44 | PRIOR = ['sources'] 45 | 46 | BASED_ON_FIELD = "BASED_ON_FIELD" 47 | SELF_ORDER = "SELF_ORDER" 48 | GET_MOUNT_FROM_PATH = "GET_MOUNT_FROM_PATH" 49 | GET_BASE_PATH = "GET_BASE_PATH" 50 | GET_PKG_FROM_SCM = "GET_PKG_FROM_SCM" 51 | PACKAGE_MANAGER = "PACKAGE_MANAGER" 52 | 53 | 54 | class Dependencies: 55 | def __init__(self, module): 56 | exec "from %s.converter import SECTION_CALL_EQ"%module 57 | exec "from %s.objects import *"%module 58 | self.__obj_maker = OBJ_MAKER 59 | self.__deps = DEPENDENCIES 60 | self.__calls = SECTION_CALL_EQ 61 | self.__handler = { 62 | BASED_ON_FIELD : self.__based_on_field, 63 | SELF_ORDER : self.__self_order, 64 | GET_MOUNT_FROM_PATH : self.__get_mount_from_path, 65 | GET_BASE_PATH : self.__get_base_path, 66 | GET_PKG_FROM_SCM : self.__get_pkg_from_scm, 67 | PACKAGE_MANAGER : self.__package_manager, 68 | } 69 | self.__data = None 70 | self.__add_obj = {} 71 | 72 | @GeneralException 73 | def run(self, data): 74 | self.__data = copy.deepcopy(data) 75 | Tools.l(INFO, "running dependency cycle generation", 'run', self) 76 | for c in self.__data: 77 | if ((c not in self.__deps) 78 | or (c not in self.__calls)): continue 79 | for obj_name in self.__data[c]: 80 | obj = self.__data[c][obj_name] 81 | for dep_name in PRIOR: 82 | if ((dep_name not in self.__data) 83 | or (dep_name not in self.__calls)): continue 84 | elif dep_name in self.__deps[c]: 85 | self.__parse_dep(c, obj_name, obj, dep_name) 86 | for dep_name in self.__deps[c]: 87 | if ((dep_name not in self.__data) 88 | or (dep_name not in self.__calls) 89 | or (dep_name in PRIOR)): continue 90 | else: self.__parse_dep(c, obj_name, obj, dep_name) 91 | if self.__add_obj: self.__data = Tools.dict_merging(self.__add_obj, self.__data) 92 | Tools.l(INFO, "dependency cycle generated", 'run', self) 93 | return self.__data 94 | 95 | @GeneralException 96 | def __parse_dep(self, c, obj_name, obj, dep_name): 97 | dep = self.__deps[c][dep_name] 98 | if type(dep) is str: 99 | obj['require'] = Tools.dict_merging(obj.get('require'), { 100 | dep : [dep_name] 101 | }) 102 | elif type(dep) is list: 103 | res = self.__handler[dep[0]](obj, dep_name, dep[1]) 104 | if res: 105 | section_dep = self.__calls[dep_name] 106 | if type(self.__calls[dep_name]) is dict: 107 | target_obj = (res[0] if type(res) is list else res) 108 | tmp_data = (Tools.dict_merging(self.__add_obj, self.__data) if self.__add_obj else self.__data) 109 | section_obj = tmp_data[dep_name].get(target_obj) 110 | if not section_obj: 111 | Tools.l(ERR, "Target object missing %s.%s"%(dep_name,target_obj), 'parse_dep', self) 112 | return 113 | section_key = section_obj.get(self.__calls[dep_name]['key']) 114 | if not section_key: 115 | Tools.l(ERR, "Section key missing for %s"%(dep_name), 'parse_dep', self) 116 | return 117 | section_dep = self.__calls[dep_name].get(section_key) 118 | if not section_dep: 119 | Tools.l(ERR, "Wrong section key %s[%s]"%(dep_name,section_key), 'parse_dep', self) 120 | return 121 | obj['require'] = Tools.dict_merging(obj.get('require'), { 122 | section_dep[len(ACTION_ID):] : res 123 | }) 124 | 125 | @GeneralException 126 | def __self_order(self, object, gclass, args): 127 | ref = object.get(args['field']) 128 | data = self.__data[gclass] 129 | if not data: return None 130 | res = None 131 | for key in sorted(data, key=lambda x: data[x][args['field']]): 132 | name = data[key][args['field']] 133 | if re.match(name, ref) and (ref != name): 134 | res = key 135 | if gclass == 'sources' and self.__data.get('dirs') and res: 136 | dirs = dict(self.__data['dirs'].items()) 137 | for dir in dirs: 138 | comp = Tools.path_basename(ref) 139 | if dirs[dir]['path'] == comp: 140 | self.__data['dirs'].pop(dir) 141 | return res 142 | 143 | @GeneralException 144 | def __get_mount_from_path(self, object, gclass, args): 145 | path = object.get(args['field']) 146 | mounts = self.__data['mounts'] 147 | if not mounts: return None 148 | res = None 149 | for key in sorted(mounts, key=lambda x: mounts[x]['name']): 150 | name = mounts[key]['name'] 151 | if re.match(name, path): 152 | res = key 153 | return res 154 | 155 | @GeneralException 156 | def __get_pkg_from_scm(self, object, gclass, args): 157 | scm = object.get(args['field']) 158 | return (SCM_EQ.get(scm) if scm else None) 159 | 160 | @GeneralException 161 | def __get_base_path(self, object, gclass, args): 162 | path = object.get(args['field']) 163 | return (Tools.path_basename(path) if path else None) 164 | 165 | @GeneralException 166 | def __based_on_field(self, object, gclass, args): 167 | if not args.get('field') or not args.get('key'): 168 | return [] 169 | res = [] 170 | v_field = (self.__handler[args['field'][0]](object, gclass, args['field'][1]) 171 | if type(args['field']) is list 172 | else object.get(args['field'])) 173 | if v_field: 174 | for obj_name in self.__data[gclass]: 175 | v_key = (args['key'][0](object, gclass, obj_name, args['key'][1]) 176 | if type(args['key']) is list 177 | else self.__data[gclass][obj_name].get(args['key'])) 178 | if (((type(v_field) is list) and (v_key in v_field)) 179 | or ((type(v_key) is list) and (v_field in v_key)) 180 | or (v_key == v_field)): 181 | res.append(obj_name) 182 | return res 183 | 184 | @GeneralException 185 | def __package_manager(self, object, gclass, args): 186 | if not args.get('field'): return [] 187 | provider = object.get(args['field']) 188 | managers = Config.managers_eq 189 | platform = Config.platform 190 | if provider and platform and managers and (provider in managers): 191 | package = managers[provider] 192 | if package not in self.__data['packages'] and managers['_autoadd']: 193 | self.__add_obj.setdefault(gclass, {}) 194 | self.__add_obj[gclass][package] = self.__obj_maker['manager'](package, platform) 195 | return [package] 196 | elif package in self.__data['packages']: 197 | return [package] 198 | return [] 199 | -------------------------------------------------------------------------------- /pysa/exception.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Exception handler 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Thibault BRONCHAIN 21 | ''' 22 | 23 | import logging 24 | 25 | class ScannerException(Exception): pass 26 | 27 | # Decorator 28 | def GeneralException(func): 29 | def __action_with_decorator(self, *args, **kwargs): 30 | try: 31 | class_name = self.__class__.__name__ 32 | func_name = func.__name__ 33 | return func(self, *args, **kwargs) 34 | except Exception, e: 35 | logging.error("%s.%s() error: %s" % (class_name, func_name, str(e))) 36 | raise ScannerException, e 37 | return __action_with_decorator 38 | -------------------------------------------------------------------------------- /pysa/filter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VisualOps/pysa/a3493826f5feb3ba59bed227fa397ce1c7916e9d/pysa/filter/__init__.py -------------------------------------------------------------------------------- /pysa/filter/filter.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Apply user filters 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Thibault BRONCHAIN 21 | ''' 22 | 23 | from pysa.tools import * 24 | from pysa.exception import * 25 | 26 | 27 | # filter actions 28 | class Filter(): 29 | def __init__(self, filters): 30 | self.f = filters 31 | 32 | # preprocessing on packages section 33 | @GeneralException 34 | def update_package(self, package, pkg_name, update): 35 | Tools.l(INFO, "selection update packages", 'update_package', self) 36 | if (not self.f) or (not self.f.get('update')): 37 | return package 38 | mode = (self.f['update']['_update'] if self.f['update'].get('_update') else False) 39 | excp = self.f['update'].get('except') 40 | if self.exception_filter(mode, excp, pkg_name, ["*", ".*"]): 41 | package['version'] = update 42 | return package 43 | 44 | # item replacement 45 | @GeneralException 46 | def item_replace(self, gclass, key, val, name, eq = None): 47 | if not self.f: 48 | return val 49 | global1 = self.f.get('replace') 50 | global2 = (global1.get(gclass) if global1 else None) 51 | section = (global2.get(key) if global2 else None) 52 | 53 | replacelist = Tools.dict_merging(Tools.dict_merging(global1, global2), section) 54 | if not replacelist: 55 | return val 56 | 57 | mode = (replacelist.pop('_replaceall') if replacelist.get('_replaceall') != None else True) 58 | excp = (replacelist.pop('_except') if replacelist.get('_except') != None else None) 59 | 60 | replacelist = Tools.dict_cleaner(replacelist) 61 | if not replacelist: 62 | return val 63 | 64 | if (excp == None 65 | or self.exception_filter(mode, excp, name, eq)): 66 | for i in replacelist: 67 | c = val 68 | for data in replacelist[i]: 69 | if (type(val) != list) and (type(val) != dict): 70 | val = re.sub("%s" % (data), 71 | "%s" % (i), 72 | "%s" % (val)) 73 | if c != val: 74 | Tools.l(INFO, 75 | "values updated for item %s in section %s" 76 | % (name, key), 77 | 'item_replace', 78 | self) 79 | return val 80 | 81 | # apply filter, replace: ["old", "new"] 82 | @GeneralException 83 | def exception_filter(self, mode, exceptions, value, exprep = None): 84 | if (exceptions == None and mode == True): 85 | return True 86 | elif (exceptions == None and mode == False): 87 | return False 88 | fl = False 89 | for name in exceptions: 90 | name = "%s$" % (name) 91 | if re.match((name.replace(exprep[0], exprep[1]) if exprep else name), value): 92 | fl = True 93 | break 94 | if (((mode == True) and (fl == False)) 95 | or ((mode == False) and (fl == True))): 96 | return True 97 | return False 98 | -------------------------------------------------------------------------------- /pysa/filter/parser.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Apply user filters 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Thibault BRONCHAIN 21 | ''' 22 | 23 | from ConfigParser import SafeConfigParser 24 | 25 | from pysa.tools import * 26 | from pysa.exception import * 27 | 28 | 29 | class ParserException(Exception): pass 30 | 31 | filters_split = ['discard', 'addition'] 32 | filters_req = { 33 | 'replace': { 34 | '_replaceall': [True, False] 35 | }, 36 | 'update': { 37 | '_update': [True, False] 38 | } 39 | } 40 | 41 | global_filters = { 42 | 'discard': { 43 | 'file' : { 44 | 'path' : [ 45 | '/etc/fstab', 46 | '/etc/group', 47 | '/etc/gshadow-', 48 | '/etc/hosts', 49 | '/etc/passwd', 50 | '/etc/passwd-', 51 | '/etc/shadow', 52 | '/etc/shadow-', 53 | ] 54 | } 55 | } 56 | } 57 | 58 | 59 | # filters parser 60 | class FParser(): 61 | def __init__(self, filename): 62 | self.__filename = Tools.file_exists(filename) 63 | 64 | # action 65 | @GeneralException 66 | def run(self): 67 | if not self.__filename: 68 | return global_filters 69 | return self.__parse_filters() 70 | 71 | # check required fields 72 | @GeneralException 73 | def __parse_req(self, sec, basename): 74 | if basename in filters_req: 75 | for req in filters_req[basename]: 76 | if sec.get(req) == None: 77 | return False 78 | elif filters_req[basename][req] == None: 79 | continue 80 | elif sec[req] not in filters_req[basename][req]: 81 | return False 82 | return True 83 | 84 | # values parsing 85 | @GeneralException 86 | def __parse_value(self, sec, key, value): 87 | if value == "true" or value == "True": 88 | sec[key] = True 89 | elif value == "false" or value == "False": 90 | sec[key] = False 91 | elif re.search(",", value): 92 | sec[key] = re.split("\s*,\s*", value) 93 | else: 94 | sec[key] = [value] 95 | return sec 96 | 97 | # parse sections 98 | @GeneralException 99 | def __parse_loop(self, parser, sec, name, refname=None): 100 | # define referer name 101 | if not refname: 102 | refname = name 103 | 104 | # get subsections 105 | keys = [refname] 106 | if re.search("\.", refname): 107 | keys = re.split("\.", refname) 108 | basename = keys[0] 109 | 110 | # create subsections 111 | curname = refname 112 | cursec = sec 113 | for key in keys: 114 | if cursec.get(key) == None: 115 | cursec[key] = {} 116 | cursec = cursec[key] 117 | 118 | # content parsing 119 | for key, value in parser.items(name): 120 | if key == '_contentrefer': 121 | if value == name: 122 | raise ParserException, ("filter file error on section %s" % refname) 123 | return self.__parse_loop(parser, sec, value, name) 124 | elif re.search("\.", key) and ((name in filters_split) or (basename in filters_split)): 125 | skey = re.split("\.", key) 126 | if cursec.get(skey[0]) == None: 127 | cursec[skey[0]] = {} 128 | cursec[skey[0]] = self.__parse_value(cursec[skey[0]], skey[1], value) 129 | else: 130 | cursec = self.__parse_value(cursec, key, value) 131 | # check required fields 132 | if (self.__parse_req(cursec, basename) == False): 133 | raise ParserException, ("filter file error on section %s" % refname) 134 | return sec 135 | 136 | # parse filters file 137 | @GeneralException 138 | def __parse_filters(self): 139 | parser = SafeConfigParser() 140 | parser.read(self.__filename) 141 | sec = {} 142 | for name in parser.sections(): 143 | sec = self.__parse_loop(parser, sec, name) 144 | return Tools.dict_merging(global_filters, sec) 145 | -------------------------------------------------------------------------------- /pysa/madeira.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Export output to Madeira account 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Thibault BRONCHAIN 21 | ''' 22 | 23 | from pysa.exception import * 24 | from pysa.tools import * 25 | 26 | # TODO 27 | # export data to madeira account 28 | class Madeira(): 29 | def __init__(self, user, user_id, output, module): 30 | self.__user = user 31 | self.__user_id = user_id 32 | self.__output = output 33 | self.__module = module 34 | 35 | # send data to Madeira account 36 | def send(self): 37 | Tools.l(ERR, "ABORTING: not yet implemented", func.__name__, self) 38 | -------------------------------------------------------------------------------- /pysa/output.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Output container 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Thibault BRONCHAIN 21 | ''' 22 | 23 | from pysa.exception import * 24 | from pysa.config import * 25 | 26 | # output container 27 | class Output(): 28 | def __init__(self): 29 | self.main = '' 30 | self.c = {} 31 | 32 | @GeneralException 33 | def add_dict(self, output, default = ''): 34 | self.c[output] = default 35 | 36 | @GeneralException 37 | def add(self, output, content): 38 | if output: 39 | self.c[output] = self.c.setdefault(output, '') 40 | self.c[output] += content 41 | else: 42 | self.main += content 43 | 44 | @GeneralException 45 | def dump(self, manifest_name=None): 46 | return (self.c[manifest_name] if manifest_name else self.main) 47 | 48 | @GeneralException 49 | def list(self): 50 | l = ([''] if self.main else []) 51 | for seq in ORDER_LIST: 52 | if seq in self.c: 53 | l.append(seq) 54 | return l 55 | 56 | @GeneralException 57 | def mod(self, content, output=None): 58 | if output: 59 | self.c[output] = content 60 | else: 61 | self.main = content 62 | -------------------------------------------------------------------------------- /pysa/preprocessing.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Data preprocessing before modules 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Thibault BRONCHAIN 21 | ''' 22 | 23 | from pysa.tools import * 24 | from pysa.exception import * 25 | from pysa.config import FILE_CLASS, ORDER_LIST 26 | 27 | from pysa.dependencies import Dependencies 28 | 29 | import copy 30 | 31 | FILE_IDENT = FILE_CLASS + [ 32 | 'sources', 33 | ] 34 | 35 | # preprocesser 36 | class Preprocessing(): 37 | def __init__(self, module): 38 | exec "from %s.objects import *"%module 39 | self.__obj_maker = OBJ_MAKER 40 | self.__cmlabel = CMLABEL 41 | self.__data = None 42 | self.__deps = Dependencies(module) 43 | 44 | # action 45 | @GeneralException 46 | def run(self, data): 47 | self.__data = copy.deepcopy(data) 48 | if self.__data: 49 | if self.__obj_maker.get('objkey'): 50 | self.__keys_mod() 51 | self.__prepross_files() 52 | self.__data = self.__deps.run(self.__data) 53 | return self.__data 54 | 55 | # create unique ids for salt 56 | @GeneralException 57 | def __keys_mod(self): 58 | new_data = {} 59 | for c in self.__data: 60 | if c not in ORDER_LIST: continue 61 | new_data[c] = {} 62 | for obj in self.__data[c]: 63 | key = self.__obj_maker['objkey'](c,obj) 64 | new_data[c][key] = self.__data[c][obj] 65 | self.__data = new_data 66 | 67 | # preprocessing on files section 68 | @GeneralException 69 | def __prepross_files(self): 70 | Tools.l(INFO, "preprocessing files", 'prepross_files', self) 71 | dds = self.__files_iter(self.__file_directory, FILE_IDENT) 72 | for file_item in dds: 73 | self.__data['dirs'] = Tools.s_dict_merging(self.__data.get('dirs'), dds[file_item]) 74 | self.__files_iter(self.__file_item_removal, ['dirs']+FILE_CLASS) 75 | Tools.l(INFO, "preprocessing files done", 'prepross_files', self) 76 | 77 | # create config files directory 78 | @GeneralException 79 | def __file_directory(self, container, file_item, files, file, files_l): 80 | if container.get(file_item) == None: 81 | container[file_item] = {} 82 | fp = files[file]['path'] 83 | drs = Tools.get_recurse_path(os.path.dirname(fp)) 84 | for dr in drs: 85 | if (dr == '/') or (("-%s"%dr) in container[file_item]): 86 | continue 87 | container[file_item]["-%s"%dr] = self.__obj_maker['file'](dr) 88 | return container 89 | 90 | # remove items 91 | @GeneralException 92 | def __file_item_removal(self, container, file_item, files, file, files_l): 93 | r_files_l = files_l[:] 94 | r_files_l.reverse() 95 | for consumed in r_files_l: 96 | if consumed == file_item: 97 | break 98 | elif self.__data.get(consumed): 99 | flag = None 100 | for f in self.__data[consumed]: 101 | path = self.__data[consumed][f]['path'] 102 | if path == files[file]['path']: 103 | flag = f 104 | break 105 | if flag: 106 | self.__data[consumed].pop(flag) 107 | return container 108 | 109 | # iterate over files 110 | @GeneralException 111 | def __files_iter(self, action, files_l): 112 | container = {} 113 | for file_item in files_l: 114 | files = (dict(self.__data[file_item].items()) if self.__data.get(file_item) else {}) 115 | for file in files: 116 | container = action(container, file_item, files, file, files_l) 117 | return container 118 | -------------------------------------------------------------------------------- /pysa/puppet/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VisualOps/pysa/a3493826f5feb3ba59bed227fa397ce1c7916e9d/pysa/puppet/__init__.py -------------------------------------------------------------------------------- /pysa/puppet/build.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Generate puppet scripts 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Thibault BRONCHAIN 21 | ''' 22 | 23 | import re 24 | 25 | from pysa.exception import * 26 | from pysa.config import * 27 | from pysa.tools import * 28 | from pysa.output import Output 29 | 30 | from pysa.puppet.converter import GLOBAL_SEC_EQ 31 | 32 | # define quoted variables 33 | QUOTED_AVOIDED_KEYS = ['content', 'before'] 34 | QUOTED_FORCED_KEYS = ['checksum', 'name', 'group', 'owner'] 35 | QUOTED_FORCED_CONTENT = ['\W', '\d'] 36 | 37 | # puppet generation class 38 | class PuppetBuild(): 39 | def __init__(self, input_dict, output_path, module_name): 40 | self.__quoted_regex = "%s" % ('|'.join(QUOTED_FORCED_CONTENT)) 41 | self.__module_name = module_name 42 | self.__input_dict = input_dict 43 | self.__output_container = Output() 44 | self.__output_path = output_path+'/'+self.__module_name 45 | self.__curent_manifest = '' 46 | 47 | # main function 48 | @GeneralException 49 | def run(self): 50 | Tools.l(INFO, "running generation engine", 'run', self) 51 | self.__generate(self.__input_dict) 52 | self.__create_init_file() 53 | self.dump_in_files() 54 | Tools.l(INFO, "generation complete", 'run', self) 55 | return True 56 | 57 | # print the puppet files 58 | @GeneralException 59 | def dump(self): 60 | for manifest_name in self.__output_container.list(): 61 | manifest_fname = (manifest_name if manifest_name else 'init') 62 | print "%s:\n%s\n\n" % (manifest_fname, 63 | self.__output_container.dump(manifest_name)) 64 | 65 | # dump puppet file in variable 66 | @GeneralException 67 | def dump_in_var(self, data=''): 68 | for manifest_name in self.__output_container.list(): 69 | manifest_fname = (manifest_name if manifest_name else 'init') 70 | data += ("%s:\n%s\n\n" % (manifest_fname, 71 | self.__output_container.dump(manifest_name))) 72 | return data 73 | 74 | # dump the puppet files into the right files 75 | @GeneralException 76 | def dump_in_files(self): 77 | for manifest_name in self.__output_container.list(): 78 | manifest_fname = (manifest_name if manifest_name else 'init') 79 | Tools.write_in_file(self.__output_path+'/manifests/'+manifest_fname+'.pp', 80 | self.__output_container.dump(manifest_name)) 81 | 82 | # init file generation 83 | @GeneralException 84 | def __create_init_file(self): 85 | includes = '' 86 | for manifest_name in self.__output_container.list(): 87 | if not manifest_name: continue 88 | includes += "include %s\n" % (manifest_name) 89 | content = '' 90 | for line in re.split('\n', 91 | self.__output_container.dump()+'\n'+includes): 92 | if not line: continue 93 | content += re.sub(r'^', r'\n\t', line) 94 | self.__output_container.mod("class %s {\n%s\n}\n" % (self.__module_name, content)) 95 | 96 | # particular case for the single instructions 97 | @GeneralException 98 | def __single_instruction(self, parent, sections, section_name, tab): 99 | if not parent: 100 | return tab 101 | for content in sections[section_name]: 102 | if section_name == GLOBAL_SEC_EQ['require']: 103 | if content not in parent: continue 104 | self.__output_container.add(self.__curent_manifest, 105 | "%s%s %s\n" % (tab,section_name[len(SINGLE_SEC):],content)) 106 | return tab 107 | 108 | # quote required values 109 | @GeneralException 110 | def __add_quotes(self, key, val): 111 | return (("'%s'" % (re.sub('\'', '\\\'', val)) 112 | if (key not in QUOTED_AVOIDED_KEYS) 113 | and ((key in QUOTED_FORCED_KEYS) 114 | or (re.search(self.__quoted_regex, val))) 115 | else val) 116 | if type(val) is str else val) 117 | 118 | # content writing 119 | @GeneralException 120 | def __write_content(self, section_name, label, optlabel, content): 121 | out = '' 122 | out_size = 0 123 | if (type(content) is list): 124 | for value in content: 125 | out += (", " if out_size else '')+"'%s'" % (value) 126 | out_size += 1 127 | if out_size: 128 | return "%s%s%s" % (("[" if out_size > 1 else ''),out,("]" if out_size > 1 else '')) 129 | elif (type(content) is dict): 130 | for value_type in content: 131 | for value in content[value_type]: 132 | out += (", " if out_size else '') + "%s['%s']" % (value_type,value) 133 | out_size += 1 134 | if out_size: 135 | return "%s%s%s" % (("[" if out_size > 1 else ''),out,("]" if out_size > 1 else '')) 136 | else: 137 | if (self.__curent_manifest in FILE_CLASS) and (label[0] != '-') and optlabel == 'content': 138 | filename = ('/' if label[0] != '/' else '')+label 139 | Tools.write_in_file(self.__output_path+'/templates'+filename, content) 140 | content = "template('%s')" % (self.__module_name+filename) 141 | return self.__add_quotes(optlabel, content) 142 | return None 143 | 144 | # global content generation for pupept config file 145 | @GeneralException 146 | def __create_content(self, parent, data, section_name, tab): 147 | Tools.l(INFO, "creating section %s" % (section_name.lstrip(VOID_EQ)), 'create_content', self) 148 | if section_name[:len(SINGLE_SEC)] == SINGLE_SEC: 149 | return self.__single_instruction(parent, data, section_name, tab) 150 | self.__output_container.add(self.__curent_manifest, "%s%s {\n" % (tab,section_name.lstrip(ACTION_ID))) 151 | for label in sorted(data[section_name]): 152 | if label in NULL: 153 | continue 154 | if label[0] != ACTION_ID: 155 | tab = Tools.tab_inc(tab) 156 | self.__output_container.add(self.__curent_manifest, "%s'%s':\n" % (tab,label)) 157 | tab = Tools.tab_inc(tab) 158 | wrote = False 159 | for optlabel in sorted(data[section_name][label]): 160 | if (data[section_name][label][optlabel] not in NULL) and (optlabel[0] != ACTION_ID): 161 | out = self.__write_content(section_name, 162 | label, 163 | optlabel, 164 | data[section_name][label][optlabel]) 165 | if out: 166 | self.__output_container.add(self.__curent_manifest, 167 | "%s%s%s => %s"%((",\n" if wrote else ''),tab,optlabel,out)) 168 | wrote = True 169 | if wrote and label != MAIN_SECTION: 170 | self.__output_container.add(self.__curent_manifest, ";\n") 171 | elif wrote and label == MAIN_SECTION: 172 | self.__output_container.add(self.__curent_manifest, "\n") 173 | tab = Tools.tab_dec(tab) 174 | if label[0] != ACTION_ID: 175 | tab = Tools.tab_dec(tab) 176 | self.__output_container.add(self.__curent_manifest, "%s}\n" % (tab)) 177 | return tab 178 | 179 | # class generation method, applies the recursion 180 | @GeneralException 181 | def __create_class(self, parent, data, section_name, tab): 182 | Tools.l(INFO, "generation class %s" % (section_name), 'create_class', self) 183 | self.__output_container.add(self.__curent_manifest, "%sclass %s {\n" % (tab,section_name)) 184 | tab = Tools.tab_inc(tab) 185 | # recursion here 186 | tab = self.__generate(data[section_name], data, tab) 187 | tab = Tools.tab_dec(tab) 188 | self.__output_container.add(self.__curent_manifest, "%s}\n%sinclude %s\n" % (tab,tab,section_name)) 189 | return tab 190 | 191 | # puppet file generation function 192 | # recursive function 193 | @GeneralException 194 | def __generate(self, data, parent = None, tab=''): 195 | # adding Exec section 196 | if GLOBAL_SEC_EQ['Exec'] in data: 197 | tab = self.__create_content(parent, data, GLOBAL_SEC_EQ['Exec'], tab) 198 | # global generation 199 | for section_name in sorted(data): 200 | # avoid exception 201 | if section_name == GLOBAL_SEC_EQ['Exec']: 202 | continue 203 | # content found 204 | elif section_name[0] == ACTION_ID and self.__curent_manifest: 205 | tab = self.__create_content(parent, data, section_name, tab) 206 | continue 207 | # new class 208 | if not parent: 209 | self.__curent_manifest = section_name 210 | self.__output_container.add_dict(self.__curent_manifest) 211 | # recursion here 212 | tab = self.__create_class(parent, data, section_name, tab) 213 | return tab 214 | -------------------------------------------------------------------------------- /pysa/puppet/converter.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Dictionnary converter for puppet scripts generation 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Thibault BRONCHAIN 21 | ''' 22 | 23 | import re 24 | import copy 25 | 26 | from pysa.tools import * 27 | from pysa.config import * 28 | from pysa.exception import * 29 | 30 | from pysa.filter.filter import Filter 31 | 32 | 33 | # define _order section 34 | 35 | # list of ordered sections 36 | ORDERED_LIST_EQ = ['sources'] 37 | 38 | # general modifiers 39 | GLOBAL_SEC_EQ = { 40 | 'Exec' : ACTION_ID+'Exec', 41 | 'exec' : ACTION_ID+'exec', 42 | 'order' : ACTION_ID+'order', 43 | 'require' : SINGLE_SEC+'require' 44 | } 45 | 46 | # define _Exec section 47 | GLOBAL_EXEC_EQ = { 48 | 'path' : '/usr/bin:/bin:/usr/sbin:/sbin' 49 | } 50 | 51 | # define _exec section 52 | EXEC_EQ = { 53 | 'apt' : 'apt-get update', 54 | 'yum' : '/usr/sbin/yum-complete-transaction', 55 | 'pip' : 'easy_install pip', 56 | } 57 | 58 | # define general sections 59 | SECTION_EQ = { 60 | 'dirs' : ACTION_ID+'file', 61 | 'files' : ACTION_ID+'file', 62 | 'packages' : ACTION_ID+'package', 63 | 'services' : ACTION_ID+'service', 64 | 'crons' : ACTION_ID+'cron', 65 | 'groups' : ACTION_ID+'group', 66 | 'mounts' : ACTION_ID+'mount', 67 | 'hosts' : ACTION_ID+'host', 68 | 'repos' : ACTION_ID+'file', 69 | 'keys' : ACTION_ID+'file', 70 | 'users' : ACTION_ID+'user', 71 | 'sources' : ACTION_ID+'vcsrepo' 72 | } 73 | SECTION_CALL_EQ = dict([(key,SECTION_EQ[key].capitalize()) for key in SECTION_EQ]) 74 | 75 | # define subsclasses equivalency 76 | SUBCLASS_EQ = { 77 | 'packages' : { 78 | MAIN_SECTION : 'provider', 79 | 'order' : [ 80 | ['apt', 'yum', 'rpm'], 81 | ['npm', 'pecl', 'pear', 'pip', 'gem'] 82 | ] 83 | } 84 | } 85 | 86 | # add 'require' instruction 87 | REQUIRE_EQ = [ 88 | SUBCLASS_EQ['packages']['order'] 89 | ] 90 | 91 | # key modifier 92 | CONTENTKEY_EQ = { 93 | MAIN_SECTION : { 94 | 'version' : 'ensure', 95 | 'key' : 'content' 96 | }, 97 | 'sources' : { 98 | 'scm' : 'provider' 99 | }, 100 | 'users' : { 101 | 'group' : 'gid' 102 | } 103 | } 104 | 105 | # val modifier (on key) 106 | CONTENTVAL_EQ = { 107 | 'packages' : { 108 | 'provider' : ['php', 'pear'] 109 | } 110 | } 111 | 112 | # content add 113 | CONTENTADD_EQ = { 114 | 'sources' : { 115 | 'ensure' : 'present' 116 | }, 117 | 'groups' : { 118 | 'ensure' : 'present' 119 | } 120 | } 121 | 122 | # avoided sections 123 | AVOIDSEC_EQ = { 124 | 'mounts' : ['size'], 125 | 'packages' : ['manager', 'config_files'], 126 | 'sources' : ['mode', 'password', 'branch', 'name', 'key'], 127 | 'groups' : ['gid'], 128 | 'users' : ['uid', 'gid'], 129 | 'repos' : ['provider'], 130 | } 131 | 132 | # Append sections 133 | APPSEC_EQ = { 134 | 'crons' : ['environment', 'PATH='] 135 | } 136 | 137 | class PuppetConverter(): 138 | def __init__(self, minput, filters = None): 139 | self.__output = {} 140 | self.__input = copy.deepcopy(minput) 141 | self.__filter = Filter(filters) 142 | self.__prev_obj = None 143 | 144 | # main method 145 | @GeneralException 146 | def run(self): 147 | Tools.l(INFO, "running", 'run', self) 148 | 149 | #empty imput 150 | if not self.__input: 151 | Tools.l(ERR, "empty input", 'run', self) 152 | return {} 153 | 154 | # convert 155 | self.__generate_classes(self.__input) 156 | 157 | # add exceptions 158 | if GLOBAL_EXEC_EQ: 159 | self.__add_global_exec() 160 | 161 | Tools.l(INFO, "complete", 'run', self) 162 | return self.__output 163 | 164 | # generate global exec 165 | @GeneralException 166 | def __add_global_exec(self): 167 | Tools.l(INFO, "adding Exec section", 'add_global_exec', self) 168 | self.__output[GLOBAL_SEC_EQ['Exec']] = {MAIN_SECTION : {}} 169 | for key in GLOBAL_EXEC_EQ: 170 | Tools.l(INFO, "adding key %s" % (key), 'add_global_exec', self) 171 | self.__output[GLOBAL_SEC_EQ['Exec']][MAIN_SECTION][key] = self.__process_values('', 'Exec', key, GLOBAL_EXEC_EQ[key]) 172 | 173 | # generate sub execs 174 | @GeneralException 175 | def __add_top_class(self, key): 176 | c = {} 177 | for order in REQUIRE_EQ: 178 | if key in order[0]: break 179 | elif key in order[1]: 180 | req = [] 181 | for r in order[0]: 182 | req.append(r) 183 | if req: 184 | c = Tools.dict_merging(c, { 185 | GLOBAL_SEC_EQ['require'] : req 186 | }) 187 | if key in EXEC_EQ: 188 | Tools.l(INFO, "adding exec section for %s" % (key), 'add_exec', self) 189 | c = Tools.dict_merging(c, { 190 | GLOBAL_SEC_EQ['exec'] : { 191 | EXEC_EQ[key] : GLOBAL_EXEC_EQ 192 | } 193 | }) 194 | return c 195 | 196 | # processing on values 197 | @GeneralException 198 | def __process_values(self, gclass, name, key, val): 199 | if type(val) is int: 200 | val = "%s" % (val) 201 | elif (type(val) is not str) or (not val): 202 | return val 203 | if (gclass in APPSEC_EQ) and (key == APPSEC_EQ[gclass][0]): 204 | val = APPSEC_EQ[gclass][1] + val 205 | return self.__filter.item_replace(gclass, key, val, name) 206 | 207 | 208 | # processing on data 209 | @GeneralException 210 | def __process_data(self, input, gclass, name, cur_class): 211 | Tools.l(INFO, "processing data", 'process_data', self) 212 | # modifications 213 | kcontent = Tools.list_merging(AVOIDSEC_EQ.get(MAIN_SECTION), AVOIDSEC_EQ.get(gclass)) 214 | for key in kcontent: 215 | if key in input: 216 | input[key] = None 217 | kcontent = Tools.s_dict_merging(CONTENTADD_EQ.get(MAIN_SECTION), CONTENTADD_EQ.get(gclass), False) 218 | for key in kcontent: 219 | input[key] = kcontent[key] 220 | kcontent = Tools.s_dict_merging(CONTENTKEY_EQ.get(MAIN_SECTION), CONTENTKEY_EQ.get(gclass), False) 221 | for key in kcontent: 222 | if key in input: 223 | input[kcontent[key]] = input.pop(key) 224 | kcontent = Tools.s_dict_merging(CONTENTVAL_EQ.get(MAIN_SECTION), CONTENTVAL_EQ.get(gclass), False) 225 | for key in kcontent: 226 | if key in input: 227 | if input[key] == kcontent[key][0]: 228 | input[key] = kcontent[key][1] 229 | 230 | # exec dependency 231 | if cur_class in EXEC_EQ: 232 | input['require'] = Tools.dict_merging(input.get('require'), { 233 | GLOBAL_SEC_EQ['exec'][len(ACTION_ID):].capitalize() : [ 234 | EXEC_EQ[cur_class] 235 | ] 236 | }) 237 | 238 | # main loop 239 | for key in input: 240 | if type(input[key]) is list: 241 | store = [] 242 | for d in input[key]: 243 | store.append(self.__process_values(gclass, name, key, d)) 244 | input[key] = store 245 | else: 246 | input[key] = self.__process_values(gclass, name, key, input[key]) 247 | return input 248 | 249 | # processing on section 250 | @GeneralException 251 | def __process_sec(self, data, gclass, name, cur_class): 252 | Tools.l(INFO, "creating section %s" % (SECTION_EQ[gclass]), 'process_sec', self) 253 | if (SECTION_EQ[gclass] == ACTION_ID+'package'): 254 | data[gclass][name] = self.__filter.update_package(data[gclass][name], name, 'latest') 255 | return self.__process_data(data[gclass][name], gclass, name, cur_class) 256 | 257 | # class generation 258 | @GeneralException 259 | def __generate_classes(self, data): 260 | for gclass in data: 261 | if gclass not in SECTION_EQ: 262 | Tools.l(INFO, "Ignored unknown class %s" % (gclass), 'generate_classes', self) 263 | continue 264 | Tools.l(INFO, "creating class %s" % (gclass), 'generate_classes', self) 265 | self.__prev_obj = None 266 | self.__output[gclass] = self.__add_top_class(gclass) 267 | for name in sorted(data[gclass]): 268 | if gclass in SUBCLASS_EQ: 269 | subkey = data[gclass][name][SUBCLASS_EQ[gclass][MAIN_SECTION]] 270 | Tools.l(INFO, "creating sub class %s" % (subkey), 'generate_classes', self) 271 | self.__output[gclass].setdefault(subkey, self.__add_top_class(subkey)) 272 | self.__output[gclass][subkey].setdefault(SECTION_EQ[gclass], {}) 273 | self.__output[gclass][subkey][SECTION_EQ[gclass]][name] = self.__process_sec(data, gclass, name, subkey) 274 | else: 275 | self.__output[gclass].setdefault(SECTION_EQ[gclass], {}) 276 | self.__output[gclass][SECTION_EQ[gclass]][name] = self.__process_sec(data, gclass, name, gclass) 277 | self.__prev_obj = name 278 | -------------------------------------------------------------------------------- /pysa/puppet/objects.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Puppet Objects 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Thibault BRONCHAIN 21 | ''' 22 | 23 | from pysa.exception import * 24 | 25 | from pysa.scanner.actions.utils import get_stat 26 | from pysa.dependencies import * 27 | 28 | CMLABEL="puppet" 29 | 30 | class PuppetObjects(): 31 | @staticmethod 32 | def puppet_file_dir_obj(dr): 33 | # get group, mode and owner 34 | s = get_stat(dr) 35 | #DEBUG 36 | #s = ('root', oct(0777), 'root') 37 | #/DEBUG 38 | return { 39 | 'path' : dr, 40 | 'ensure' : 'directory', 41 | 'name' : dr, 42 | 'group' : s[0], 43 | 'mode' : s[1], 44 | 'owner' : s[2], 45 | } 46 | 47 | @staticmethod 48 | def puppet_pkg_manager_obj(package, provider): 49 | return { 50 | 'name' : package, 51 | 'provider' : provider, 52 | 'version' : 'latest', 53 | } 54 | 55 | 56 | OBJ_MAKER = { 57 | 'file' : PuppetObjects.puppet_file_dir_obj, 58 | 'manager' : PuppetObjects.puppet_pkg_manager_obj, 59 | } 60 | 61 | DEPENDENCIES = { 62 | 'hosts' : {}, 63 | 'mounts' : { 64 | 'hosts' : 'Class', 65 | 'mounts' : [ 66 | BASED_ON_FIELD, { 67 | 'field' : [SELF_ORDER, { 68 | 'field' : 'name', 69 | }], 70 | 'key' : 'name', 71 | } 72 | ], 73 | }, 74 | 'groups' : { 75 | 'hosts' : 'Class', 76 | }, 77 | 'users' : { 78 | 'hosts' : 'Class', 79 | 'groups' : [ 80 | BASED_ON_FIELD, { 81 | 'field' : 'group', 82 | 'key' : 'name', 83 | } 84 | ], 85 | }, 86 | 'dirs' : { 87 | 'hosts' : 'Class', 88 | 'dirs' : [ 89 | BASED_ON_FIELD, { 90 | 'field' : [SELF_ORDER, { 91 | 'field' : 'path', 92 | }], 93 | 'key' : 'path', 94 | } 95 | ], 96 | 'groups' : [ 97 | BASED_ON_FIELD, { 98 | 'field' : 'group', 99 | 'key' : 'name', 100 | } 101 | ], 102 | 'users' : [ 103 | BASED_ON_FIELD, { 104 | 'field' : 'owner', 105 | 'key' : 'name', 106 | } 107 | ], 108 | 'mounts' : [ 109 | BASED_ON_FIELD, { 110 | 'field' : [GET_MOUNT_FROM_PATH, { 111 | 'field' : 'path', 112 | }], 113 | 'key' : 'device', 114 | } 115 | ], 116 | }, 117 | 'keys' : { 118 | 'hosts' : 'Class', 119 | 'groups' : [ 120 | BASED_ON_FIELD, { 121 | 'field' : 'group', 122 | 'key' : 'name', 123 | } 124 | ], 125 | 'users' : [ 126 | BASED_ON_FIELD, { 127 | 'field' : 'owner', 128 | 'key' : 'name', 129 | } 130 | ], 131 | 'dirs' : [ 132 | BASED_ON_FIELD, { 133 | 'field' : [GET_BASE_PATH, { 134 | 'field' : 'path', 135 | }], 136 | 'key' : 'path', 137 | } 138 | ], 139 | }, 140 | 'repos' : { 141 | 'hosts' : 'Class', 142 | 'groups' : [ 143 | BASED_ON_FIELD, { 144 | 'field' : 'group', 145 | 'key' : 'name', 146 | } 147 | ], 148 | 'users' : [ 149 | BASED_ON_FIELD, { 150 | 'field' : 'owner', 151 | 'key' : 'name', 152 | } 153 | ], 154 | 'dirs' : [ 155 | BASED_ON_FIELD, { 156 | 'field' : [GET_BASE_PATH, { 157 | 'field' : 'path', 158 | }], 159 | 'key' : 'path', 160 | } 161 | ], 162 | }, 163 | 'packages' : { 164 | 'hosts' : 'Class', 165 | 'repos' : [ 166 | BASED_ON_FIELD, { 167 | 'field' : 'provider', 168 | 'key' : 'provider', 169 | } 170 | ], 171 | 'dirs' : [ 172 | BASED_ON_FIELD, { 173 | 'field' : [GET_BASE_PATH, { 174 | 'field' : 'path', 175 | }], 176 | 'key' : 'path', 177 | } 178 | ], 179 | }, 180 | 'files' : { 181 | 'hosts' : 'Class', 182 | 'groups' : [ 183 | BASED_ON_FIELD, { 184 | 'field' : 'group', 185 | 'key' : 'name', 186 | } 187 | ], 188 | 'users' : [ 189 | BASED_ON_FIELD, { 190 | 'field' : 'owner', 191 | 'key' : 'name', 192 | } 193 | ], 194 | 'dirs' : [ 195 | BASED_ON_FIELD, { 196 | 'field' : [GET_BASE_PATH, { 197 | 'field' : 'path', 198 | }], 199 | 'key' : 'path', 200 | } 201 | ], 202 | 'packages' : [ 203 | BASED_ON_FIELD, { 204 | 'field' : 'path', 205 | 'key' : 'config_files', 206 | } 207 | ], 208 | }, 209 | 'crons' : { 210 | 'hosts' : 'Class', 211 | 'users' : [ 212 | BASED_ON_FIELD, { 213 | 'field' : 'user', 214 | 'key' : 'name', 215 | } 216 | ], 217 | }, 218 | 'sources' : { 219 | 'hosts' : 'Class', 220 | 'sources' : [ 221 | BASED_ON_FIELD, { 222 | 'field' : [SELF_ORDER, { 223 | 'field' : 'path', 224 | }], 225 | 'key' : 'path', 226 | } 227 | ], 228 | 'groups' : [ 229 | BASED_ON_FIELD, { 230 | 'field' : 'group', 231 | 'key' : 'name', 232 | } 233 | ], 234 | 'users' : [ 235 | BASED_ON_FIELD, { 236 | 'field' : 'owner', 237 | 'key' : 'name', 238 | } 239 | ], 240 | 'dirs' : [ 241 | BASED_ON_FIELD, { 242 | 'field' : [GET_BASE_PATH, { 243 | 'field' : 'path', 244 | }], 245 | 'key' : 'path', 246 | } 247 | ], 248 | 'keys' : [ 249 | BASED_ON_FIELD, { 250 | 'field' : 'key', 251 | 'key' : 'name', 252 | } 253 | ], 254 | 'packages' : [ 255 | BASED_ON_FIELD, { 256 | 'field' : [GET_PKG_FROM_SCM, { 257 | 'field' : 'scm', 258 | }], 259 | 'key' : 'name', 260 | } 261 | ], 262 | }, 263 | 'services' : { 264 | 'files' : 'Class', 265 | }, 266 | } 267 | -------------------------------------------------------------------------------- /pysa/salt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VisualOps/pysa/a3493826f5feb3ba59bed227fa397ce1c7916e9d/pysa/salt/__init__.py -------------------------------------------------------------------------------- /pysa/salt/build.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Generate salt manifests 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Thibault BRONCHAIN 21 | ''' 22 | 23 | import re 24 | 25 | from pysa.exception import * 26 | from pysa.config import * 27 | from pysa.tools import * 28 | from pysa.output import Output 29 | 30 | # salt generation class 31 | class SaltBuild(): 32 | def __init__(self, input_dict, output_path, module_name): 33 | self.__module_name = module_name 34 | self.__input_dict = input_dict 35 | self.__output_container = Output() 36 | self.__output_path = output_path+'/'+self.__module_name 37 | self.__curent_manifest = None 38 | self.__curent_state = None 39 | self.__curent_name = None 40 | 41 | # main function 42 | @GeneralException 43 | def run(self): 44 | Tools.l(INFO, "running generation engine", 'run', self) 45 | self.__generate(self.__input_dict) 46 | self.__create_init_file() 47 | self.dump_in_files() 48 | Tools.l(INFO, "generation complete", 'run', self) 49 | return True 50 | 51 | # print the puppet files 52 | @GeneralException 53 | def dump(self): 54 | for manifest_name in self.__output_container.list(): 55 | manifest_fname = (manifest_name if manifest_name else 'init') 56 | print "%s:\n%s\n\n" % (manifest_fname, 57 | self.__output_container.dump(manifest_name)) 58 | 59 | # dump puppet file in variable 60 | @GeneralException 61 | def dump_in_var(self, data=''): 62 | for manifest_name in self.__output_container.list(): 63 | manifest_fname = (manifest_name if manifest_name else 'init') 64 | data += ("%s:\n%s\n\n" % (manifest_fname, 65 | self.__output_container.dump(manifest_name))) 66 | return data 67 | 68 | # dump the puppet files into the right files 69 | @GeneralException 70 | def dump_in_files(self): 71 | for manifest_name in self.__output_container.list(): 72 | manifest_fname = (manifest_name if manifest_name else 'init') 73 | Tools.write_in_file(self.__output_path+'/'+manifest_fname+'.sls', 74 | self.__output_container.dump(manifest_name)) 75 | 76 | # init file generation 77 | @GeneralException 78 | def __create_init_file(self): 79 | self.__output_container.add(None, "include:\n") 80 | for manifest in self.__output_container.list(): 81 | if not manifest: continue 82 | self.__output_container.add(None, " - %s.%s\n"%(self.__module_name,manifest)) 83 | 84 | # content writing 85 | @GeneralException 86 | def __write_content(self, key, val, tab): 87 | if (self.__curent_manifest in FILE_CLASS) and key == 'source': 88 | name = self.__input_dict[self.__curent_manifest][self.__curent_state][self.__curent_name]['name'] 89 | filename = "%s" % (('/' if name[0] != '/' else '')+name) 90 | Tools.write_in_file(self.__output_path+'/templates'+filename, val) 91 | val = "salt://%s" % (self.__module_name+'/templates'+filename) 92 | self.__output_container.add(self.__curent_manifest, "%s"%(val)) 93 | 94 | # section generation (recursive) 95 | @GeneralException 96 | def __create_section(self, key, val, tab): 97 | if (key in NULL) or (val == None): return 98 | self.__output_container.add(self.__curent_manifest, "%s- %s"%(tab,key)) 99 | if val == MAIN_SECTION: 100 | self.__output_container.add(self.__curent_manifest, "\n") 101 | elif type(val) is dict: 102 | tab += " " 103 | self.__output_container.add(self.__curent_manifest, ":\n") 104 | for sub_key in val: 105 | if key == "require": 106 | for itm in val[sub_key]: 107 | self.__create_section("%s: %s"%(sub_key,itm), MAIN_SECTION, tab) 108 | else: 109 | self.__create_section(sub_key, val[sub_key], tab) 110 | elif type(val) is list: 111 | tab += " " 112 | self.__output_container.add(self.__curent_manifest, ":\n") 113 | for d in val: 114 | self.__create_section(d, MAIN_SECTION, tab) 115 | else: 116 | self.__output_container.add(self.__curent_manifest, ": ") 117 | self.__write_content(key, val, tab) 118 | self.__output_container.add(self.__curent_manifest, "\n") 119 | 120 | # global content generation for salt config file 121 | @GeneralException 122 | def __create_content(self, data, manifest, state, name): 123 | cur_data = data[manifest][state][name] 124 | self.__output_container.add(self.__curent_manifest, "%s:\n"%(name if name[0] != '-' else name[1:])) 125 | self.__output_container.add(self.__curent_manifest, " %s:\n"%(state[len(ACTION_ID):])) 126 | for key in cur_data: 127 | self.__create_section(key, cur_data[key], " ") 128 | 129 | # puppet file generation function 130 | @GeneralException 131 | def __generate(self, data): 132 | # global generation 133 | for manifest in sorted(data): 134 | Tools.l(INFO, "generation manifest %s" % (manifest), 'generate', self) 135 | self.__curent_manifest = manifest 136 | for state in sorted(data[manifest]): 137 | self.__curent_state = state 138 | Tools.l(INFO, "module state %s" % (state), 'generate', self) 139 | for name in sorted(data[manifest][state]): 140 | self.__curent_name = name 141 | Tools.l(INFO, "item %s" % (name), 'generate', self) 142 | self.__create_content(data, manifest, state, name) 143 | -------------------------------------------------------------------------------- /pysa/salt/converter.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Dictionnary converter for salt scripts generation 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Thibault BRONCHAIN 21 | ''' 22 | 23 | import re 24 | import hashlib 25 | import copy 26 | 27 | from pysa.tools import * 28 | from pysa.config import * 29 | from pysa.exception import * 30 | 31 | from pysa.filter.filter import Filter 32 | 33 | 34 | def handler_files_checksum(old, content): 35 | contents = ['content','key','source'] 36 | c = None 37 | for c_name in contents: 38 | c = content.get(c_name) 39 | if c: break 40 | if not c: return None 41 | return "md5=%s"%(hashlib.md5(c).hexdigest()) 42 | 43 | def handler_actionkey_pkg(content): 44 | if (content.get('version') == 'latest') or not content.get('version'): 45 | content['version'] = None 46 | return 'latest' 47 | return 'installed' 48 | 49 | def handler_hosts_names(old, content): 50 | return (old if type(old) is list else [old]) 51 | 52 | # TODO: sources 53 | 54 | # define general sections 55 | SECTION_EQ = { 56 | # 'dirs' : ACTION_ID+'file', 57 | 'files' : ACTION_ID+'file', 58 | 'packages' : { 59 | 'key' : 'provider', 60 | 'apt' : ACTION_ID+'pkg', 61 | 'yum' : ACTION_ID+'pkg', 62 | 'rpm' : ACTION_ID+'pkg', 63 | # 'php' : ACTION_ID+'pecl', 64 | 'pecl' : ACTION_ID+'pecl', 65 | 'pear' : ACTION_ID+'pecl', 66 | 'pip' : ACTION_ID+'pip', 67 | 'npm' : ACTION_ID+'npm', 68 | 'gem' : ACTION_ID+'gem', 69 | }, 70 | 'services' : ACTION_ID+'service', 71 | 'crons' : ACTION_ID+'cron', 72 | 'groups' : ACTION_ID+'group', 73 | 'mounts' : ACTION_ID+'mount', 74 | 'hosts' : ACTION_ID+'host', 75 | 'repos' : ACTION_ID+'file', 76 | 'keys' : ACTION_ID+'file', 77 | 'users' : ACTION_ID+'user', 78 | # 'sources' : { 79 | # 'key' : 'provider', 80 | # 'git' : ACTION_ID+'git', 81 | # 'svn' : ACTION_ID+'svn', 82 | # 'hg' : ACTION_ID+'hg', 83 | # } 84 | } 85 | SECTION_CALL_EQ = SECTION_EQ 86 | 87 | # avoided sections 88 | AVOIDSEC_EQ = { 89 | 'cron' : ['environment', 'name', 'target'], 90 | 'files' : ['provider','recurse','recurselimit','source'], 91 | 'groups' : ['member','gid'], 92 | 'hosts' : ['target'], 93 | 'mounts' : ['atboot','size'], 94 | ACTION_ID+'pkg' : ['config_files','description','responsefile','provider','instance','category','platform','root','manager','vendor'], 95 | ACTION_ID+'pecl' : ['version','config_files','description','responsefile','provider','instance','category','platform','root','manager','vendor'], 96 | ACTION_ID+'gem' : ['version','config_files','description','responsefile','provider','instance','category','platform','root','manager','vendor'], 97 | ACTION_ID+'npm' : ['version','config_files','description','responsefile','provider','instance','category','platform','root','manager','vendor'], 98 | ACTION_ID+'pip' : ['version','config_files','description','responsefile','provider','instance','category','platform','root','manager','vendor'], 99 | 'repos' : ['provider','recurse','recurselimit','source'], 100 | 'services' : ['hasrestart','path','provider','binary','control','ensure','hasstatus','manifest','start','stop','restart'], 101 | 'keys' : ['target','host_aliases','type','name'], 102 | 'users' : ['uid', 'gid', 'expiry'], 103 | } 104 | # content add 105 | CONTENTADD_EQ = { 106 | 'files' : { 107 | 'makedirs' : 'True', 108 | }, 109 | 'mounts' : { 110 | 'mkmnt' : 'True', 111 | }, 112 | 'repos' : { 113 | 'makedirs' : 'True', 114 | }, 115 | 'keys' : { 116 | 'source_hash' : 'md5', 117 | }, 118 | 'services' : { 119 | 'enable' : 'True', 120 | }, 121 | } 122 | # key modifier 123 | CONTENTKEY_EQ = { 124 | MAIN_SECTION : { 125 | }, 126 | 'crons' : { 127 | 'command' : 'name', 128 | 'monthday' : 'daymonth', 129 | 'weekday' : 'dayweek', 130 | }, 131 | 'files' : { 132 | 'checksum' : 'source_hash', 133 | 'content' : 'source', 134 | 'owner' : 'user', 135 | 'path' : 'name', 136 | 'force' : 'replace', 137 | }, 138 | 'hosts' : { 139 | 'name' : 'names', 140 | 'host_aliases' : 'names', 141 | }, 142 | 'mount' : { 143 | 'remounts' : 'remount', 144 | 'options' : 'opts', 145 | }, 146 | 'repos' : { 147 | 'checksum' : 'source_hash', 148 | 'content' : 'source', 149 | 'owner' : 'user', 150 | 'path' : 'name', 151 | 'force' : 'replace', 152 | }, 153 | 'keys' : { 154 | 'key' : 'source', 155 | 'content' : 'source', 156 | 'path' : 'name', 157 | }, 158 | 'users' : { 159 | 'group' : 'gid', 160 | } 161 | } 162 | # val modifier (on key) 163 | CONTENTVAL_EQ = { 164 | 'files' : { 165 | 'source_hash' : [MAIN_SECTION,handler_files_checksum], 166 | }, 167 | 'repos' : { 168 | 'source_hash' : [MAIN_SECTION,handler_files_checksum], 169 | }, 170 | 'keys' : { 171 | 'source_hash' : [MAIN_SECTION,handler_files_checksum], 172 | }, 173 | 'hosts' : { 174 | 'names' : [MAIN_SECTION,handler_hosts_names], 175 | }, 176 | } 177 | 178 | # action key 179 | ACTIONKEY_EQ = { 180 | 'crons' : 'present', 181 | 'files' : 'managed', 182 | 'groups' : 'present', 183 | 'hosts' : 'present', 184 | 'mounts' : 'mounted', 185 | ACTION_ID+'pkg' : handler_actionkey_pkg, 186 | ACTION_ID+'pecl' : 'installed', 187 | ACTION_ID+'gem' : 'installed', 188 | ACTION_ID+'npm' : 'installed', 189 | ACTION_ID+'pip' : 'installed', 190 | 'repos' : 'managed', 191 | 'services' : 'running', 192 | 'keys' : 'managed', 193 | 'users' : 'present', 194 | } 195 | 196 | # Append sections 197 | APPSEC_EQ = { 198 | } 199 | 200 | 201 | class SaltConverter(): 202 | def __init__(self, minput, filters=None): 203 | self.__output = {} 204 | self.__input = copy.deepcopy(minput) 205 | self.__filter = Filter(filters) 206 | 207 | # main method 208 | @GeneralException 209 | def run(self): 210 | Tools.l(INFO, "running", 'run', self) 211 | 212 | #empty imput 213 | if not self.__input: 214 | Tools.l(ERR, "empty input", 'run', self) 215 | return {} 216 | 217 | self.__curent_state = None 218 | 219 | # convert 220 | self.__generate_classes(self.__input) 221 | 222 | Tools.l(INFO, "complete", 'run', self) 223 | return self.__output 224 | 225 | # processing on values 226 | @GeneralException 227 | def __process_values(self, manifest, name, key, val): 228 | if type(val) is int: 229 | val = "%s" % (val) 230 | elif (type(val) is not str) or (not val): 231 | return val 232 | if (manifest in APPSEC_EQ) and (key == APPSEC_EQ[manifest][0]): 233 | val = APPSEC_EQ[manifest][1] + val 234 | return self.__filter.item_replace(manifest, key, val, name) 235 | 236 | # processing on data 237 | # recursive 238 | @GeneralException 239 | def __structure_data(self, data, manifest, name): 240 | Tools.l(INFO, "building data structure", 'structure_data', self) 241 | for key in data: 242 | if type(data[key]) is dict: 243 | data[key] = self.__structure_data(data[key], manifest, name) 244 | elif type(data[key]) is list: 245 | store = [] 246 | for d in data[key]: 247 | store.append(self.__process_values(manifest, name, key, d)) 248 | data[key] = store 249 | else: 250 | data[key] = self.__process_values(manifest, name, key, data[key]) 251 | Tools.l(INFO, "building data structure complete", 'structure_data', self) 252 | return data 253 | 254 | # processing on data 255 | @GeneralException 256 | def __process_data(self, data, manifest, name): 257 | Tools.l(INFO, "processing data", 'process_data', self) 258 | sec_key = (self.__curent_state if type(SECTION_EQ[manifest]) is dict else manifest) 259 | 260 | # modifications 261 | kcontent = Tools.list_merging(AVOIDSEC_EQ.get(MAIN_SECTION), AVOIDSEC_EQ.get(sec_key)) 262 | for key in kcontent: 263 | if key in data: 264 | data[key] = None 265 | kcontent = Tools.s_dict_merging(CONTENTADD_EQ.get(MAIN_SECTION), CONTENTADD_EQ.get(sec_key), False) 266 | for key in kcontent: 267 | data[key] = kcontent[key] 268 | kcontent = Tools.s_dict_merging(CONTENTKEY_EQ.get(MAIN_SECTION), CONTENTKEY_EQ.get(sec_key), False) 269 | for key in kcontent: 270 | if key in data: 271 | data[kcontent[key]] = Tools.merge_string_list(data.get(kcontent[key]), data.pop(key)) 272 | kcontent = Tools.s_dict_merging(CONTENTVAL_EQ.get(MAIN_SECTION), CONTENTVAL_EQ.get(sec_key), False) 273 | for key in kcontent: 274 | if key in data: 275 | if (data[key] == kcontent[key][0]) or (kcontent[key][0] == MAIN_SECTION): 276 | if type(kcontent[key][1]) is str: 277 | data[key] = kcontent[key][1] 278 | else: 279 | data[key] = kcontent[key][1](data[key], data) 280 | 281 | # set action key 282 | kcontent = ACTIONKEY_EQ.get(sec_key) 283 | if type(kcontent) is str: 284 | data[kcontent] = MAIN_SECTION 285 | elif kcontent: 286 | data[kcontent(data)] = MAIN_SECTION 287 | 288 | # main loop 289 | data = self.__structure_data(data, manifest, name) 290 | Tools.l(INFO, "processing data complete", 'process_data', self) 291 | return data 292 | 293 | # ganaration method 294 | @GeneralException 295 | def __generate_classes(self, data): 296 | for manifest in sorted(data): 297 | if manifest not in SECTION_EQ: 298 | Tools.l(INFO, "Ignored unknown class %s" % (manifest), 'generate_classes', self) 299 | continue 300 | Tools.l(INFO, "creating manifest %s" % (manifest), 'generate_classes', self) 301 | for name in sorted(data[manifest]): 302 | if type(SECTION_EQ[manifest]) is dict: 303 | ref = data[manifest][name].get(SECTION_EQ[manifest]['key']) 304 | if not ref: 305 | Tools.l(ERR, "Reference key not found %s" % (SECTION_EQ[manifest]['key']), 'generate_classes', self) 306 | continue 307 | state = SECTION_EQ[manifest].get(ref) 308 | if not state: 309 | Tools.l(ERR, "State not found ref %s, manifest %s"%(ref,manifest), 'generate_classes', self) 310 | continue 311 | else: 312 | state = SECTION_EQ.get(manifest) 313 | if not state: 314 | Tools.l(ERR, "State not found manifest %s"%(manifest), 'generate_classes', self) 315 | continue 316 | if (manifest == 'packages'): 317 | data[manifest][name] = self.__filter.update_package(data[manifest][name], name, 'latest') 318 | self.__output.setdefault(manifest, {}) 319 | self.__output[manifest].setdefault(state, {}) 320 | self.__curent_state = state 321 | self.__output[manifest][state][name] = self.__process_data(data[manifest][name], manifest, name) 322 | -------------------------------------------------------------------------------- /pysa/salt/objects.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Salt Objects 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Thibault BRONCHAIN 21 | ''' 22 | 23 | import re 24 | 25 | from pysa.exception import * 26 | from pysa.dependencies import * 27 | 28 | from pysa.scanner.actions.utils import get_stat 29 | 30 | CMLABEL="salt" 31 | ILLEGAL_OBJ_CHAR=None#['\W'] 32 | 33 | class SaltObjects(): 34 | @staticmethod 35 | def salt_file_dir_obj(dr): 36 | # get group, mode and owner 37 | s = get_stat(dr) 38 | #DEBUG 39 | #s = ('root', oct(0777), 'root') 40 | #/DEBUG 41 | return { 42 | 'path' : dr, 43 | 'ensure' : 'directory', 44 | 'name' : dr, 45 | 'group' : s[0], 46 | 'mode' : s[1], 47 | 'owner' : s[2], 48 | } 49 | 50 | @staticmethod 51 | def salt_pkg_manager_obj(package, provider): 52 | return { 53 | 'name' : package, 54 | 'provider' : provider, 55 | 'version' : 'latest', 56 | } 57 | 58 | @staticmethod 59 | def salt_key_mod_obj(state, obj): 60 | key = "%s_%s"%(state,obj) 61 | if not ILLEGAL_OBJ_CHAR: return key 62 | illegal_char = "%s" % ('|'.join(ILLEGAL_OBJ_CHAR)) 63 | key = re.sub(illegal_char, "_", key) 64 | return key 65 | 66 | 67 | OBJ_MAKER = { 68 | 'file' : SaltObjects.salt_file_dir_obj, 69 | 'manager' : SaltObjects.salt_pkg_manager_obj, 70 | 'objkey' : SaltObjects.salt_key_mod_obj, 71 | } 72 | 73 | DEPENDENCIES = { 74 | 'hosts' : {}, 75 | 'mounts' : { 76 | # 'hosts' : 'Class', 77 | 'mounts' : [ 78 | BASED_ON_FIELD, { 79 | 'field' : [SELF_ORDER, { 80 | 'field' : 'name', 81 | }], 82 | 'key' : 'name', 83 | } 84 | ], 85 | }, 86 | 'groups' : { 87 | # 'hosts' : 'Class', 88 | }, 89 | 'users' : { 90 | # 'hosts' : 'Class', 91 | 'groups' : [ 92 | BASED_ON_FIELD, { 93 | 'field' : 'group', 94 | 'key' : 'name', 95 | } 96 | ], 97 | }, 98 | # 'dirs' : { 99 | # 'hosts' : 'Class', 100 | # 'dirs' : [ 101 | # BASED_ON_FIELD, { 102 | # 'field' : [SELF_ORDER, { 103 | # 'field' : 'path', 104 | # }], 105 | # 'key' : 'path', 106 | # } 107 | # ], 108 | # 'groups' : [ 109 | # BASED_ON_FIELD, { 110 | # 'field' : 'group', 111 | # 'key' : 'name', 112 | # } 113 | # ], 114 | # 'users' : [ 115 | # BASED_ON_FIELD, { 116 | # 'field' : 'owner', 117 | # 'key' : 'name', 118 | # } 119 | # ], 120 | # 'mounts' : [ 121 | # BASED_ON_FIELD, { 122 | # 'field' : [GET_MOUNT_FROM_PATH, { 123 | # 'field' : 'path', 124 | # }], 125 | # 'key' : 'device', 126 | # } 127 | # ], 128 | # }, 129 | 'keys' : { 130 | # 'hosts' : 'Class', 131 | 'groups' : [ 132 | BASED_ON_FIELD, { 133 | 'field' : 'group', 134 | 'key' : 'name', 135 | } 136 | ], 137 | 'users' : [ 138 | BASED_ON_FIELD, { 139 | 'field' : 'owner', 140 | 'key' : 'name', 141 | } 142 | ], 143 | # 'dirs' : [ 144 | # BASED_ON_FIELD, { 145 | # 'field' : [GET_BASE_PATH, { 146 | # 'field' : 'path', 147 | # }], 148 | # 'key' : 'path', 149 | # } 150 | # ], 151 | }, 152 | 'repos' : { 153 | # 'hosts' : 'Class', 154 | 'groups' : [ 155 | BASED_ON_FIELD, { 156 | 'field' : 'group', 157 | 'key' : 'name', 158 | } 159 | ], 160 | 'users' : [ 161 | BASED_ON_FIELD, { 162 | 'field' : 'owner', 163 | 'key' : 'name', 164 | } 165 | ], 166 | # 'dirs' : [ 167 | # BASED_ON_FIELD, { 168 | # 'field' : [GET_BASE_PATH, { 169 | # 'field' : 'path', 170 | # }], 171 | # 'key' : 'path', 172 | # } 173 | # ], 174 | }, 175 | 'packages' : { 176 | # 'hosts' : 'Class', 177 | 'repos' : [ 178 | BASED_ON_FIELD, { 179 | 'field' : 'provider', 180 | 'key' : 'provider', 181 | } 182 | ], 183 | 'packages' : [ 184 | PACKAGE_MANAGER, { 185 | 'field' : 'provider', 186 | 'key' : None, 187 | } 188 | ], 189 | # 'dirs' : [ 190 | # BASED_ON_FIELD, { 191 | # 'field' : [GET_BASE_PATH, { 192 | # 'field' : 'path', 193 | # }], 194 | # 'key' : 'path', 195 | # } 196 | # ], 197 | }, 198 | 'files' : { 199 | # 'hosts' : 'Class', 200 | 'groups' : [ 201 | BASED_ON_FIELD, { 202 | 'field' : 'group', 203 | 'key' : 'name', 204 | } 205 | ], 206 | 'users' : [ 207 | BASED_ON_FIELD, { 208 | 'field' : 'owner', 209 | 'key' : 'name', 210 | } 211 | ], 212 | # 'dirs' : [ 213 | # BASED_ON_FIELD, { 214 | # 'field' : [GET_BASE_PATH, { 215 | # 'field' : 'path', 216 | # }], 217 | # 'key' : 'path', 218 | # } 219 | # ], 220 | 'packages' : [ 221 | BASED_ON_FIELD, { 222 | 'field' : 'path', 223 | 'key' : 'config_files', 224 | } 225 | ], 226 | }, 227 | 'crons' : { 228 | # 'hosts' : 'Class', 229 | 'users' : [ 230 | BASED_ON_FIELD, { 231 | 'field' : 'user', 232 | 'key' : 'name', 233 | } 234 | ], 235 | }, 236 | 'sources' : { 237 | # 'hosts' : 'Class', 238 | 'sources' : [ 239 | BASED_ON_FIELD, { 240 | 'field' : [SELF_ORDER, { 241 | 'field' : 'path', 242 | }], 243 | 'key' : 'path', 244 | } 245 | ], 246 | 'groups' : [ 247 | BASED_ON_FIELD, { 248 | 'field' : 'group', 249 | 'key' : 'name', 250 | } 251 | ], 252 | 'users' : [ 253 | BASED_ON_FIELD, { 254 | 'field' : 'owner', 255 | 'key' : 'name', 256 | } 257 | ], 258 | # 'dirs' : [ 259 | # BASED_ON_FIELD, { 260 | # 'field' : [GET_BASE_PATH, { 261 | # 'field' : 'path', 262 | # }], 263 | # 'key' : 'path', 264 | # } 265 | # ], 266 | 'keys' : [ 267 | BASED_ON_FIELD, { 268 | 'field' : 'key', 269 | 'key' : 'name', 270 | } 271 | ], 272 | 'packages' : [ 273 | BASED_ON_FIELD, { 274 | 'field' : [GET_PKG_FROM_SCM, { 275 | 'field' : 'scm', 276 | }], 277 | 'key' : 'name', 278 | } 279 | ], 280 | }, 281 | 'services' : { 282 | # how to identify services? 283 | }, 284 | } 285 | -------------------------------------------------------------------------------- /pysa/scanner/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VisualOps/pysa/a3493826f5feb3ba59bed227fa397ce1c7916e9d/pysa/scanner/__init__.py -------------------------------------------------------------------------------- /pysa/scanner/actions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VisualOps/pysa/a3493826f5feb3ba59bed227fa397ce1c7916e9d/pysa/scanner/actions/__init__.py -------------------------------------------------------------------------------- /pysa/scanner/actions/base.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-3-27 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Ken 21 | ''' 22 | 23 | import subprocess 24 | import logging 25 | import re 26 | 27 | from pysa.exception import * 28 | from pysa.scanner.object.package import Package 29 | from pysa.scanner.object.file import File 30 | from pysa.scanner.object.user import User 31 | from pysa.scanner.object.service import Service 32 | from pysa.scanner.object.repository import Repository 33 | from pysa.scanner.object.group import Group 34 | from pysa.scanner.object.cron import Cron 35 | from pysa.scanner.object.host import Host 36 | from pysa.scanner.object.mount import Mount 37 | from pysa.scanner.object.sshkey import SSHKey 38 | from pysa.scanner.object.source import Source 39 | from pysa.scanner.object.process import Process 40 | 41 | 42 | class ScannerBase(): 43 | def __init__(self, packages, files, crons, groups, mounts, hosts, repos, services, sshkeys, users, ips, sources, proces): 44 | self.packages = packages 45 | self.files = files 46 | self.crons = crons 47 | self.groups = groups 48 | self.mounts = mounts 49 | self.hosts = hosts 50 | self.repos = repos 51 | self.services = services 52 | self.keys = sshkeys 53 | self.users = users 54 | self.ips = ips 55 | self.sources = sources 56 | self.proces = proces 57 | 58 | def scan(self): 59 | ''' 60 | Implement in ScannerYum, ScannerApt... 61 | ''' 62 | pass 63 | 64 | @GeneralException 65 | def subprocess(self, command): 66 | logging.info('ScannerBase.subprocess(): Command, %s' % str(command)) 67 | try: 68 | # redirect the err to /dev/null 69 | devnull = open('/dev/null', 'w') 70 | p = subprocess.Popen( 71 | command, 72 | close_fds=True, 73 | stdout=subprocess.PIPE, 74 | stderr=devnull 75 | ) 76 | return self.__generator(p.stdout) 77 | except OSError: 78 | logging.debug("ScannerBase.subprocess(): commnand failed, command: %s" % str(command)) 79 | return 80 | 81 | @GeneralException 82 | def __generator(self, stdout): 83 | for line in stdout: 84 | yield line 85 | 86 | @GeneralException 87 | def add_package(self, *args, **kargs): 88 | _package = Package(*args, **kargs) 89 | 90 | if self.rules: 91 | # attribute filter 92 | if _package.attr_filter(self.rules['discard']): return 93 | # additional filter 94 | _package.add_filter(self.rules['addition']) 95 | 96 | self.packages[_package.primaryvalue] = _package.prase() 97 | 98 | @GeneralException 99 | def get_packages(self): 100 | return self.packages 101 | 102 | @GeneralException 103 | def add_file(self, *args, **kargs): 104 | _file = File(*args, **kargs) 105 | 106 | if self.rules: 107 | if _file.attr_filter(self.rules['discard']): return 108 | _file.add_filter(self.rules['addition']) 109 | 110 | self.files[_file.primaryvalue] = _file.prase() 111 | 112 | @GeneralException 113 | def get_files(self): 114 | return self.files 115 | 116 | @GeneralException 117 | def add_user(self, *args, **kargs): 118 | _user = User(*args, **kargs) 119 | 120 | if self.rules: 121 | if _user.attr_filter(self.rules['discard']): return 122 | _user.add_filter(self.rules['addition']) 123 | 124 | self.users[_user.primaryvalue] = _user.prase() 125 | 126 | @GeneralException 127 | def get_users(self): 128 | return self.users 129 | 130 | @GeneralException 131 | def add_service(self, *args, **kargs): 132 | _service = Service(*args, **kargs) 133 | 134 | if self.rules: 135 | if _service.attr_filter(self.rules['discard']): return 136 | _service.add_filter(self.rules['addition']) 137 | 138 | self.services[_service.primaryvalue] = _service.prase() 139 | 140 | @GeneralException 141 | def get_services(self): 142 | return self.services 143 | 144 | @GeneralException 145 | def add_repo(self, *args, **kargs): 146 | _repo = Repository(*args, **kargs) 147 | 148 | if self.rules: 149 | if _repo.attr_filter(self.rules['discard']): return 150 | _repo.add_filter(self.rules['addition']) 151 | 152 | self.repos[_repo.primaryvalue] = _repo.prase() 153 | 154 | @GeneralException 155 | def get_repos(self): 156 | return self.repos 157 | 158 | @GeneralException 159 | def add_group(self, *args, **kargs): 160 | _group = Group(*args, **kargs) 161 | 162 | if self.rules: 163 | if _group.attr_filter(self.rules['discard']): return 164 | _group.add_filter(self.rules['addition']) 165 | 166 | self.groups[_group.primaryvalue] = _group.prase() 167 | 168 | @GeneralException 169 | def get_groups(self): 170 | return self.groups 171 | 172 | @GeneralException 173 | def add_cron(self, *args, **kargs): 174 | _cron = Cron(*args, **kargs) 175 | 176 | if self.rules: 177 | if _cron.attr_filter(self.rules['discard']): return 178 | _cron.add_filter(self.rules['addition']) 179 | 180 | self.crons[_cron.primaryvalue] = _cron.prase() 181 | 182 | @GeneralException 183 | def get_crons(self): 184 | return self.crons 185 | 186 | @GeneralException 187 | def add_host(self, *args, **kargs): 188 | _host = Host(*args, **kargs) 189 | 190 | if self.rules: 191 | if _host.attr_filter(self.rules['discard']): return 192 | _host.add_filter(self.rules['addition']) 193 | 194 | self.hosts[_host.primaryvalue] = _host.prase() 195 | 196 | @GeneralException 197 | def get_hosts(self): 198 | return self.hosts 199 | 200 | @GeneralException 201 | def add_mount(self, *args, **kargs): 202 | _mount = Mount(*args, **kargs) 203 | 204 | if self.rules: 205 | if _mount.attr_filter(self.rules['discard']): return 206 | _mount.add_filter(self.rules['addition']) 207 | 208 | self.mounts[_mount.primaryvalue] = _mount.prase() 209 | 210 | @GeneralException 211 | def get_mounts(self): 212 | return self.mounts 213 | 214 | @GeneralException 215 | def add_key(self, *args, **kargs): 216 | _key = SSHKey(*args, **kargs) 217 | 218 | if self.rules: 219 | if _key.attr_filter(self.rules['discard']): return 220 | _key.add_filter(self.rules['addition']) 221 | 222 | self.keys[_key.primaryvalue] = _key.prase() 223 | 224 | @GeneralException 225 | def get_keys(self): 226 | return self.keys 227 | 228 | @GeneralException 229 | def add_ip(self, mip): 230 | self.ips.append(mip) 231 | 232 | @GeneralException 233 | def get_ips(self): 234 | return self.ips 235 | 236 | @GeneralException 237 | def add_source(self, *args, **kargs): 238 | _source = Source(*args, **kargs) 239 | 240 | if self.rules: 241 | if _source.attr_filter(self.rules['discard']): return 242 | _source.add_filter(self.rules['addition']) 243 | 244 | self.sources[_source.primaryvalue] = _source.prase() 245 | 246 | @GeneralException 247 | def get_sources(self): 248 | return self.sources 249 | 250 | @GeneralException 251 | def add_proc(self, *args, **kargs): 252 | _process = Process(*args, **kargs) 253 | 254 | if self.rules: 255 | if _process.attr_filter(self.rules['discard']): return 256 | _process.add_filter(self.rules['addition']) 257 | 258 | self.proces[_process.primaryvalue] = _process.prase() 259 | 260 | @GeneralException 261 | def get_proces(self): 262 | return self.proces 263 | 264 | @GeneralException 265 | def init_filter(self, rules=None): 266 | """ 267 | init the filter rules 268 | """ 269 | 270 | self.rules = rules 271 | 272 | if self.rules: # init the discard and addition rules if not 273 | if 'discard' not in self.rules: self.rules['discard'] = {} 274 | if 'addition' not in self.rules: self.rules['addition'] = {} 275 | -------------------------------------------------------------------------------- /pysa/scanner/actions/cron.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Scan cron files 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Michael 21 | ''' 22 | 23 | import logging 24 | import os 25 | 26 | from pysa.scanner.actions.base import ScannerBase 27 | 28 | 29 | class ScannerCron(ScannerBase): 30 | def scan(self): 31 | 32 | users = self.get_users() 33 | res = self.subprocess(['crontab', '-l']) 34 | for line in res: 35 | # ignore empty and the comment lines 36 | if not line.strip() or line.strip().startswith("#"): continue 37 | 38 | ary = line.split() 39 | if ary[5] in users.keys(): 40 | paths = os.path.split(ary[6]) 41 | self.add_cron( 42 | name=paths[1], 43 | command=" ".join(ary[6:]), 44 | environment=paths[0], 45 | user=ary[5], 46 | minute=ary[0], 47 | hour=ary[1], 48 | monthday=ary[2], 49 | month=ary[3], 50 | weekday=ary[4] 51 | ) 52 | else: 53 | paths = os.path.split(ary[5]) 54 | self.add_cron( 55 | name=paths[1], 56 | command=" ".join(ary[5:]), 57 | environment=paths[0], 58 | minute=ary[0], 59 | hour=ary[1], 60 | monthday=ary[2], 61 | month=ary[3], 62 | weekday=ary[4] 63 | ) 64 | -------------------------------------------------------------------------------- /pysa/scanner/actions/file.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-3-31 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Michael 21 | ''' 22 | 23 | import os 24 | import re 25 | import logging 26 | 27 | from pysa.config import * 28 | from pysa.scanner.actions.utils import * 29 | from pysa.scanner.actions.base import ScannerBase 30 | 31 | 32 | class ScannerFile(ScannerBase): 33 | 34 | suf_list = ['.conf', '.cfg', '.ini'] 35 | 36 | def scan(self): 37 | """ 38 | scan config files 39 | """ 40 | 41 | logging.info('searching for config files') 42 | 43 | # scan the system directories 44 | self.scandir(Config.files_path) 45 | 46 | # scan directory and add config files 47 | def scandir(self, pathdir): 48 | # Visit every file in pathdir except those on the exclusion list above. 49 | pathdirs = re.split(":", pathdir) 50 | for p in pathdirs: 51 | if not p: continue 52 | for dirpath, dirnames, filenames in os.walk(p, followlinks=True): 53 | for filename in filenames: 54 | self.addfile(os.path.join(dirpath, filename)) 55 | 56 | # add per config file 57 | def addfile(self, pathname): 58 | # only plane text file 59 | if valid_txtfile(pathname) == False: 60 | return 61 | 62 | # # only include above suffix config file 63 | # suf = os.path.splitext(pathname)[1] 64 | # if suf is None or suf not in self.suf_list: 65 | # return 66 | 67 | # get owner, group and mode 68 | s = get_stat(pathname) 69 | 70 | # read the config file's content 71 | c = get_content(pathname) 72 | 73 | # add the config file: 74 | # checksum, content, group, mode, owner, path, force=False, provider=None, 75 | # recurse=None, recurselimit=None, source=None 76 | self.add_file('md5', c, s[0], s[1], s[2], pathname) 77 | -------------------------------------------------------------------------------- /pysa/scanner/actions/gem.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-3-29 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Michael 21 | ''' 22 | 23 | import glob 24 | import logging 25 | import re 26 | import os 27 | 28 | from pysa.scanner.actions.utils import * 29 | from pysa.scanner.actions.base import ScannerBase 30 | 31 | class ScannerGem(ScannerBase): 32 | 33 | def scan(self): 34 | """ 35 | scan gem 36 | """ 37 | logging.info('searching for Ruby gems') 38 | 39 | # Precompile a pattern for extracting the version of Ruby that was used 40 | # to install the gem. 41 | pattern = re.compile(r'gems/([^/]+)/gems') 42 | 43 | # Look for gems in all the typical places. This is easier than looking 44 | # for `gem` commands, which may or may not be on `PATH`. 45 | for globname in ('/usr/lib/ruby/gems/*/gems', 46 | '/usr/lib64/ruby/gems/*/gems', 47 | '/usr/local/lib/ruby/gems/*/gems', 48 | '/var/lib/gems/*/gems'): 49 | for dirname in glob.glob(globname): 50 | # The `ruby1.9.1` (really 1.9.2) package on Maverick begins 51 | # including RubyGems in the `ruby1.9.1` package and marks the 52 | # `rubygems1.9.1` package as virtual. So for Maverick and 53 | # newer, the manager is actually `ruby1.9.1`. 54 | match = pattern.search(dirname) 55 | if '1.9.1' == match.group(1) and rubygems_virtual(): 56 | manager = 'ruby{0}'.format(match.group(1)) 57 | 58 | # Oneiric and RPM-based distros just have one RubyGems package. 59 | elif rubygems_unversioned(): 60 | manager = 'rubygems' 61 | 62 | # Debian-based distros qualify the package name with the version 63 | # of Ruby it will use. 64 | else: 65 | manager = 'rubygems{0}'.format(match.group(1)) 66 | 67 | for entry in os.listdir(dirname): 68 | try: 69 | package, version = entry.rsplit('-', 1) 70 | 71 | except ValueError: 72 | logging.warning('skipping questionably named gem {0}'. 73 | format(entry)) 74 | continue 75 | 76 | self.add_package(package, manager=manager, version=version, provider='gem') 77 | -------------------------------------------------------------------------------- /pysa/scanner/actions/group.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-4-4 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Ken 21 | ''' 22 | 23 | import logging 24 | import grp 25 | 26 | from pysa.scanner.actions.base import ScannerBase 27 | 28 | 29 | class ScannerGroup(ScannerBase): 30 | 31 | def scan(self): 32 | for g in grp.getgrall(): 33 | name, password, gid, member = g 34 | self.add_group(name=name, gid=gid, member=member) 35 | -------------------------------------------------------------------------------- /pysa/scanner/actions/host.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-04-03 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Michael 21 | ''' 22 | 23 | import re 24 | import logging 25 | import os 26 | 27 | from pysa.config import * 28 | from pysa.scanner.actions.base import ScannerBase 29 | 30 | 31 | class ScannerHost(ScannerBase): 32 | 33 | def scan(self): 34 | """ 35 | scan host: 36 | parse the host config file (usually /etc/hosts) 37 | """ 38 | logging.info('searching for hosts') 39 | hostlst = self.parse_hostfile() 40 | 41 | if len(hostlst)<=0: 42 | return 43 | 44 | for dict in hostlst: 45 | self.add_host(ip=dict['ip'], name=dict['name'], 46 | target=dict['target'], host_aliases=dict['host_aliases']) 47 | 48 | def parse_hostfile(self): 49 | hostfile = Config.scan_host 50 | 51 | hosts = [] 52 | try: 53 | for line in open(hostfile): 54 | # ignore blank line 55 | if not line.strip(): continue 56 | # ignore comment line 57 | elif line.strip().startswith("#"): continue 58 | 59 | itemlst = line.strip().split() 60 | if len(itemlst) <= 1: continue 61 | 62 | hosts.append({'ip':itemlst[0], 'name':itemlst[1], 'target':hostfile, 'host_aliases':itemlst[2:]}) 63 | 64 | #ip = re.search( r'[0-9]+(?:\.[0-9]+){3}', itemlst[0] ) 65 | #if ip==None: 66 | # continue 67 | #aliases = [] 68 | #if len(itemlst)>=3: 69 | # for i in range(2, len(itemlst)-1): 70 | # aliases.append(itemlst[i]) 71 | #print "aliases=%s"%aliases 72 | #hosts.append({'ip':ip.group(), 'name':itemlst[1], 'target':hostfile, 'host_aliases':aliases}) 73 | 74 | except IOError: 75 | return hosts 76 | 77 | return hosts 78 | -------------------------------------------------------------------------------- /pysa/scanner/actions/mount.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Scans mount points 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Michael 21 | ''' 22 | 23 | import logging 24 | import os 25 | 26 | from pysa.scanner.actions.base import ScannerBase 27 | 28 | 29 | class ScannerMount(ScannerBase): 30 | 31 | def scan(self): 32 | self.__disk_usage() 33 | 34 | def __disk_partitions(self,all=False): 35 | """ 36 | Return all mountd partitions as a dict. 37 | """ 38 | phydevs = [] 39 | f = open("/proc/filesystems", "r") 40 | for line in f: 41 | if not line.startswith("nodev"): 42 | phydevs.append(line.strip()) 43 | 44 | retlist = {} 45 | f = open('/etc/mtab', "r") 46 | for line in f: 47 | if not all and line.startswith('none'): 48 | continue 49 | fields = line.split() 50 | device = fields[0] 51 | mountpoint = fields[1] 52 | fstype = fields[2] 53 | if not all and fstype not in phydevs: 54 | continue 55 | if device == 'none': 56 | device = '' 57 | retlist[device] = (device, mountpoint, fstype) 58 | return retlist 59 | 60 | def __disk_usage(self): 61 | """Return disk usage associated with path.""" 62 | disk = self.__disk_partitions() 63 | for device in disk.keys(): 64 | st = os.statvfs(device) 65 | size = st.f_blocks * st.f_frsize 66 | self.add_mount(device=device, fstype=disk[device][2], name=disk[device][1], size=size) 67 | -------------------------------------------------------------------------------- /pysa/scanner/actions/npm.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-3-29 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Michael 21 | ''' 22 | 23 | import re 24 | import logging 25 | 26 | from pysa.scanner.actions.base import ScannerBase 27 | 28 | 29 | class ScannerNpm(ScannerBase): 30 | def scan(self): 31 | """ 32 | scan apt 33 | """ 34 | logging.info('searching for npm packages') 35 | 36 | # Precompile a pattern for parsing the output of `npm list -g`. 37 | pattern = re.compile(r'^\S+ (\S+)@(\S+)$') 38 | 39 | lines = self.subprocess(['npm', 'ls', '-g']) # only list global packages 40 | for line in lines: 41 | match = pattern.match(line.rstrip()) 42 | if match is None: 43 | continue 44 | package, version = match.group(1), match.group(2) 45 | manager='nodejs' 46 | 47 | self.add_package(package, manager=manager, version=version, provider='npm') 48 | -------------------------------------------------------------------------------- /pysa/scanner/actions/php.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-3-29 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Michael 21 | ''' 22 | 23 | import logging 24 | import re 25 | 26 | from pysa.scanner.actions.utils import * 27 | from pysa.scanner.actions.base import ScannerBase 28 | 29 | 30 | class ScannerPhp(ScannerBase): 31 | def scan(self): 32 | """ 33 | scan php 34 | """ 35 | logging.info('searching for PEAR/PECL packages') 36 | 37 | # Precompile a pattern for parsing the output of `{pear,pecl} list`. 38 | pattern = re.compile(r'^([0-9a-zA-Z_]+)\s+([0-9][0-9a-zA-Z\.-]*)\s') 39 | 40 | # PEAR packages are managed by `php-pear` (obviously). PECL packages 41 | # are managed by `php5-dev` because they require development headers 42 | # (less obvious but still makes sense). 43 | if lsb_release_codename() is None: 44 | pecl_manager = 'php-devel' 45 | else: 46 | pecl_manager = 'php5-dev' 47 | for manager, progname in (('php-pear', 'pear'), 48 | (pecl_manager, 'pecl')): 49 | 50 | lines = self.subprocess([progname, 'list']) 51 | if lines==None: 52 | return 53 | 54 | for line in lines: 55 | match = pattern.match(line) 56 | if match is None: 57 | continue 58 | package, version = match.group(1), match.group(2) 59 | 60 | self.add_package(package, manager=manager, version=version, provider=progname) 61 | -------------------------------------------------------------------------------- /pysa/scanner/actions/process.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-04-19 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Michael 21 | ''' 22 | 23 | import logging 24 | import subprocess 25 | 26 | from pysa.scanner.actions.base import ScannerBase 27 | 28 | 29 | class ScannerProcess(ScannerBase): 30 | 31 | def scan(self): 32 | """ 33 | scan process 34 | """ 35 | logging.info('searching for process') 36 | 37 | self.get_processes() 38 | 39 | def get_processes(self): 40 | """ 41 | get all the process info 42 | """ 43 | 44 | try: 45 | p = subprocess.Popen(['-c', 'ps -eo pid,fuser,s,pcpu,pmem,comm,ppid'], shell=True, stdout=subprocess.PIPE) 46 | 47 | first = True 48 | for line in p.stdout: 49 | if first: # ignore the headline 50 | first = False 51 | continue 52 | 53 | lst = line.strip().split() 54 | 55 | # check data completeness 56 | nonlst = [info for info in lst if info is None] 57 | if len(nonlst)>0: continue 58 | 59 | self.add_proc(lst[0], lst[1], lst[2], lst[3], lst[4], lst[5], lst[6]) 60 | 61 | except OSError: 62 | return 63 | 64 | def get_accounts(self): 65 | """ 66 | parse the system accounts config file('/etc/passwd') 67 | """ 68 | 69 | try: 70 | data = open('/etc/passwd').read() 71 | for line in data: 72 | att = line.strip().split(':') 73 | if att[0] is not None and att[2] is not None: 74 | self.add_accout(att[2], att[0]) 75 | except IOError: 76 | return False 77 | 78 | return True 79 | 80 | @property 81 | def accounts(self): 82 | if accounts not in self: 83 | self.accounts = {} 84 | 85 | return self.accounts 86 | 87 | def add_accout(self, uid, name): 88 | if uid not in self.accounts: 89 | self.accounts[uid] = name 90 | -------------------------------------------------------------------------------- /pysa/scanner/actions/pypi.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-3-29 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Michael 21 | ''' 22 | 23 | import glob 24 | import logging 25 | import os 26 | import re 27 | import subprocess 28 | 29 | from pysa.scanner.actions.base import ScannerBase 30 | 31 | 32 | # Precompile a pattern to extract the manager from a pathname. 33 | PATTERN_MANAGER = re.compile(r'lib/(python[^/]*)/(dist|site)-packages') 34 | 35 | # Precompile patterns for differentiating between packages built by 36 | # `easy_install` and packages built by `pip`. 37 | PATTERN_EGG = re.compile(r'\.egg$') 38 | PATTERN_EGGINFO = re.compile(r'\.egg-info$') 39 | 40 | # Precompile a pattern for extracting package names and version numbers. 41 | PATTERN = re.compile(r'^([^-]+)-([^-]+).*\.egg(-info)?$') 42 | 43 | class ScannerPypi(ScannerBase): 44 | 45 | def scan(self): 46 | """ 47 | scan pypi 48 | """ 49 | logging.info('searching for Python packages') 50 | 51 | # Look for packages in the typical places. 52 | globnames = ['/usr/lib/python*/dist-packages', 53 | '/usr/lib/python*/site-packages', 54 | '/usr/local/lib/python*/dist-packages', 55 | '/usr/local/lib/python*/site-packages'] 56 | virtualenv = os.getenv('VIRTUAL_ENV') 57 | if virtualenv is not None: 58 | globnames.extend(['{0}/lib/python*/dist-packages'.format(virtualenv), 59 | '{0}/lib/python*/dist-packages'.format(virtualenv)]) 60 | for globname in globnames: 61 | for dirname in glob.glob(globname): 62 | manager = PATTERN_MANAGER.search(dirname).group(1) 63 | for entry in os.listdir(dirname): 64 | match = PATTERN.match(entry) 65 | if match is None: 66 | continue 67 | package, version = match.group(1, 2) 68 | 69 | pathname = os.path.join(dirname, entry) 70 | 71 | # Symbolic links indicate this is actually a system package 72 | # that injects files into the PYTHONPATH. 73 | if os.path.islink(pathname): continue 74 | 75 | # check pathname 76 | if not os.path.isdir(pathname): 77 | pathname = os.path.join(dirname, package) 78 | if not os.path.exists(pathname): continue 79 | 80 | # installed via `easy_install`. 81 | if PATTERN_EGG.search(entry): 82 | self.add_package(package, manager='python', version=version, provider='pip') 83 | 84 | # installed via `pip`. 85 | elif PATTERN_EGGINFO.search(entry): 86 | self.add_package(package, manager='pip', version=version, provider='pip') 87 | -------------------------------------------------------------------------------- /pysa/scanner/actions/repository.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-04-18 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Michael 21 | ''' 22 | 23 | import os 24 | import logging 25 | 26 | from pysa.scanner.actions.utils import * 27 | from pysa.scanner.actions.base import ScannerBase 28 | 29 | 30 | class ScannerRepo(ScannerBase): 31 | 32 | def scan(self): 33 | """ 34 | scan for repository config files 35 | """ 36 | logging.info('searching for repository config files') 37 | 38 | if os.path.exists('/etc/yum.repos.d'): 39 | self.scan_yum() 40 | elif os.path.exists('/etc/apt'): 41 | self.scan_apt() 42 | 43 | def scan_yum(self): 44 | """ 45 | scan yum repo config files 46 | """ 47 | 48 | for dirpath, dirnames, files in os.walk('/etc/yum.repos.d'): 49 | for file in files: 50 | root, ext = os.path.splitext(file) 51 | if ext!='.repo': continue 52 | 53 | self.addfile(os.path.join(dirpath, file), 'yum') 54 | 55 | def scan_apt(self): 56 | """ 57 | scan apt repo config files 58 | """ 59 | 60 | try: 61 | with open('/etc/apt/sources.list'): self.addfile('/etc/apt/sources.list', 'apt') 62 | except IOError: 63 | return 64 | 65 | if os.path.exists('/etc/apt/sources.list.d'): 66 | for dirpath, dirnames, files in os.walk('/etc/apt/sources.list.d'): 67 | for file in files: 68 | root, ext = os.path.splitext(file) 69 | if ext!='.list': continue 70 | 71 | self.addfile(os.path.join(dirpath, file), 'apt') 72 | 73 | 74 | # add per config file 75 | def addfile(self, pathname, prov): 76 | # only plane text file 77 | if valid_txtfile(pathname) == False: 78 | return 79 | 80 | # get owner, group and mode 81 | s = get_stat(pathname) 82 | 83 | # read the config file's content 84 | c = get_content(pathname) 85 | 86 | # add the config file: 87 | # checksum, content, group, mode, owner, path, force=False, provider=None, 88 | # recurse=None, recurselimit=None, source=None 89 | self.add_repo('md5', c, s[0], s[1], s[2], pathname, provider=prov) 90 | -------------------------------------------------------------------------------- /pysa/scanner/actions/service.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-04-03 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Michael 21 | ''' 22 | 23 | import re 24 | import logging 25 | import os 26 | 27 | from pysa.config import * 28 | from pysa.scanner.actions.base import ScannerBase 29 | 30 | 31 | class ScannerService(ScannerBase): 32 | # Patterns for determining which Upstart services should be included, based 33 | # on the events used to start them. 34 | UPSTART_PATTERN1 = re.compile(r'start\s+on\s+runlevel\s+\[[2345]', re.S) 35 | UPSTART_PATTERN2 = re.compile(r'start\s+on\s+\([^)]*(?:filesystem|filesystems|local-filesystems|mounted|net-device-up|remote-filesystems|startup|virtual-filesystems)[^)]*\)', re.S) 36 | 37 | def scan(self): 38 | """ 39 | scan service: 40 | search the service config files 41 | """ 42 | logging.info('searching for system services') 43 | 44 | for dir in ['/etc/init', 45 | '/etc/init.d', 46 | '/etc/rc.d/init.d']: 47 | for dirname, dirnames, filenames in os.walk(dir): 48 | for filename in filenames: 49 | try: 50 | pathname = os.path.join(dirname, filename) 51 | dict = self.parse_service(pathname) 52 | 53 | # add service 54 | if dict != None: 55 | # format (provider, name, enable, hasrestart) 56 | self.add_service(enable=dict['enable'], hasrestart=dict['hasrestart'], 57 | name=dict['name'], path=dirname, provider=dict['provider'], hasstatus=dict['hasstatus']) 58 | except ValueError: 59 | pass 60 | 61 | def parse_service(self, pathname): 62 | """ 63 | Parse a potential service init script or config file into the 64 | manager and service name or raise `ValueError`. Use the Upstart 65 | "start on" stanzas and SysV init's LSB headers to restrict services to 66 | only those that start at boot and run all the time. 67 | 68 | ###Need to add systemd service parse. 69 | """ 70 | 71 | dirname, basename = os.path.split(pathname) 72 | if '/etc/init' == dirname: 73 | service, ext = os.path.splitext(basename) 74 | 75 | # Ignore extraneous files in /etc/init. 76 | if '.conf' != ext: 77 | raise ValueError('not an Upstart config') 78 | 79 | # Ignore services that don't operate on the (faked) main runlevels. 80 | try: 81 | content = open(pathname).read() 82 | except IOError: 83 | raise ValueError('not a readable Upstart config') 84 | 85 | enable = False 86 | if (self.UPSTART_PATTERN1.search(content) \ 87 | or self.UPSTART_PATTERN2.search(content)): 88 | enable = True 89 | 90 | return {'provider':'upstart', 'name':service, 'enable':enable, 91 | 'hasrestart':False, 'hasstatus':False} 92 | 93 | elif '/etc/init.d' == dirname or '/etc/rc.d/init.d' == dirname: 94 | #import pdb 95 | #pdb.set_trace() 96 | 97 | # Let Upstart handle its services. 98 | if os.path.islink(pathname) \ 99 | and '/lib/init/upstart-job' == os.readlink(pathname): 100 | raise ValueError('proxy for an Upstart config') 101 | 102 | # Ignore services that don't operate on the main runlevels. 103 | try: 104 | content = open(pathname).read() 105 | except IOError: 106 | raise ValueError('not a readable SysV init script') 107 | 108 | enable = False 109 | if re.search(r'(?:Default-Start|chkconfig):\s*[-2345]', content): 110 | enable = True 111 | 112 | hasrestart = False 113 | if re.search(r'\s*(?:restart|reload)\)\s*', content): 114 | hasrestart = True 115 | 116 | hasstatus = False 117 | if re.search(r'\s*status\)\s*', content): 118 | hasstatus = True 119 | 120 | return {'provider':'init', 'name':basename, 'enable':enable, 121 | 'hasrestart':hasrestart, 'hasstatus':hasstatus} ### change sysvinit to init 122 | else: 123 | raise ValueError('not a service') 124 | -------------------------------------------------------------------------------- /pysa/scanner/actions/source.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-04-09 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Michael 21 | ''' 22 | 23 | import os 24 | import logging 25 | import subprocess 26 | from collections import defaultdict 27 | 28 | from pysa.scanner.actions.utils import * 29 | from pysa.scanner.actions.base import ScannerBase 30 | 31 | 32 | class ScannerSource(ScannerBase): 33 | 34 | def scan(self): 35 | """ 36 | scan repository 37 | """ 38 | logging.info('searching for repository') 39 | 40 | self.user_repos = {'git':[], 'svn':[], 'hg':[]} # save all the scm repos 41 | 42 | self.search_scm('/') 43 | #self.search_scm('svn', '/') 44 | #self.search_scm('hg', '/') 45 | self.add_repos() 46 | 47 | def search_scm(self, dir): 48 | """ 49 | search all scm repositories in local directory dirname 50 | """ 51 | for dirpath, dirnames, filenames in os.walk(dir): 52 | for dirname in dirnames: 53 | if dirname == '.git': 54 | scm = 'git' 55 | elif dirname == '.svn': 56 | scm = 'svn' 57 | elif dirname == '.hg': 58 | scm = 'hg' 59 | else: 60 | continue 61 | 62 | if scm=='svn': #if svn scm, need to check subdirctory 63 | head, tail = os.path.split(dirpath) 64 | while head and tail: 65 | if head in self.user_repos['svn']: 66 | break 67 | head, tail = os.path.split(head) 68 | else: 69 | subsvn = [ scmdir for scmdir in self.user_repos['svn'] if dirpath in scmdir ] 70 | if len(subsvn)>0: 71 | for subdir in subsvn: self.user_repos['svn'].remove(subdir) 72 | 73 | self.add_local_repo(scm, dirpath) 74 | else: 75 | self.add_local_repo(scm, dirpath) 76 | 77 | def add_repos(self): 78 | """ 79 | get the repository info and add repo 80 | """ 81 | for (scm, dirs) in self.user_repos.items(): 82 | for dirname in dirs: 83 | sources = [] # http/ssh 84 | branches = [] 85 | 86 | try: 87 | if scm=='git': 88 | p = subprocess.Popen(['-c', 'git --git-dir=' + dirname + '/.git --work-tree=' + dirname+' remote -v'], 89 | stdout=subprocess.PIPE, shell=True) 90 | for line in p.stdout: 91 | src = line.split('\t')[1].split(' ')[0] 92 | if src is not None and src not in sources: 93 | sources.append(src) 94 | 95 | # branches 96 | p = subprocess.Popen(['-c', 'git --git-dir=' + dirname + '/.git --work-tree=' + dirname + ' branch'], 97 | stdout=subprocess.PIPE, shell=True) 98 | for line in p.stdout: 99 | if line is not None: 100 | lst = line.strip().split() 101 | for br in lst: 102 | if br is not None and br!='*': 103 | branches.append(br) 104 | 105 | elif scm=='svn': 106 | p = subprocess.Popen(['-c', 'svn info ' + dirname + ' | grep URL | awk \'{print $NF}\''], 107 | shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 108 | 109 | for source in p.stdout: 110 | if source is not None: sources.append(source.strip()) 111 | 112 | ### branches 113 | elif scm=='hg': 114 | # sources 115 | p = subprocess.Popen(['-c', 'hg paths -R' + dirname], shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 116 | for line in p.stdout: 117 | source = line.split('=')[1].strip() 118 | if source is not None and source not in sources: sources.append(source) 119 | # branches 120 | p = subprocess.Popen(['-c', 'hg branches -R' + dirname], shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 121 | for line in p.stdout: 122 | branch = line.strip().split(' ')[0] 123 | if branch is not None and branch not in branches: branches.append(branch) 124 | else: 125 | continue 126 | 127 | except OSError: 128 | continue 129 | 130 | # add the public info 131 | s = get_stat(dirname) 132 | 133 | if len(sources)<=0: continue 134 | 135 | self.add_source(sources[0], os.path.basename(dirname), dirname, s[2], s[0], s[1], scm, branches) 136 | 137 | @property 138 | def user_repos(self): 139 | """ 140 | local repos 141 | """ 142 | if user_repos not in self: 143 | self.user_repos = defaultdict(list) 144 | 145 | return self.user_repos 146 | 147 | def add_local_repo(self, scm, dirname): 148 | """ 149 | add all repos to local repos 150 | """ 151 | if scm not in self.user_repos: 152 | self.user_repos[scm] = [] 153 | 154 | self.user_repos[scm].append(dirname) 155 | -------------------------------------------------------------------------------- /pysa/scanner/actions/sshkey.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Get SSH keys 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Michael 21 | ''' 22 | 23 | import logging 24 | import os 25 | import re 26 | 27 | from pysa.config import Config 28 | from pysa.scanner.actions.base import ScannerBase 29 | from pysa.scanner.actions.utils import * 30 | 31 | 32 | class ScannerKey(ScannerBase): 33 | 34 | support_key_type = [ 35 | 'ssh-rsa', 36 | 'ssh-dss', 37 | 'ecdsa-sha2-nistp256', 38 | 'ecdsa-sha2-nistp384', 39 | 'ecdsa-sha2-nistp521' 40 | ] 41 | 42 | scan_file_type = [ 43 | '.pub', 44 | '.pem' 45 | ] 46 | 47 | re_pattern = "-----.+-----" 48 | 49 | path_dir = Config.key_path 50 | 51 | def scan(self): 52 | pathdirs = re.split(":", self.path_dir) 53 | for p in pathdirs: 54 | if not p: continue 55 | for dirpath, dirnames, filenames in os.walk(p): 56 | for filename in filenames: 57 | try: 58 | # key with specific name 59 | if '.' in filename: 60 | for file_type in self.scan_file_type: 61 | if filename.endswith(file_type): 62 | full_path = os.path.join(dirpath, filename) 63 | mode = get_stat(full_path)[1] 64 | content = get_content(full_path) 65 | if content: 66 | _type = self.__get_type(content) 67 | self.add_key(key=content, name=filename, _type=_type, path=full_path, mode=mode) 68 | logging.debug('ScannerKey.scan(): Add key file %s' % filename) 69 | # key without specific name 70 | else: 71 | full_path = os.path.join(dirpath, filename) 72 | mode = get_stat(full_path)[1] 73 | content = get_content(full_path) 74 | if content and re.match(self.re_pattern, content): 75 | _type = self.__get_type(content) 76 | self.add_key(key=content, name=filename, _type=_type, path=full_path, mode=mode) 77 | logging.debug('ScannerKey.scan(): Add key file %s' % filename) 78 | except Exception, e: 79 | #log 80 | logging.error("ScannerKey.scan(): Add file %s failed, %s" % (filename, str(e))) 81 | 82 | def __get_type(self, content): 83 | for _type in self.support_key_type: 84 | if _type in content: 85 | return _type 86 | -------------------------------------------------------------------------------- /pysa/scanner/actions/user.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-4-4 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Ken 21 | ''' 22 | 23 | import logging 24 | import pwd 25 | import grp 26 | 27 | from pysa.scanner.actions.base import ScannerBase 28 | 29 | 30 | class ScannerUser(ScannerBase): 31 | def scan(self): 32 | for p in pwd.getpwall(): 33 | name, password, uid, gid, gecos, home, shell = p 34 | 35 | groups = [] 36 | # get the secondary groups 37 | for gr in grp.getgrall(): 38 | gr_name, gr_pwd, gr_gid, gr_mem = gr 39 | # check whether the main group 40 | if gid == gr_gid: 41 | group = gr_name 42 | continue 43 | # check whether the group member 44 | if name in gr_mem: 45 | groups.append(gr_name) 46 | 47 | if group is None or not group: continue 48 | 49 | self.add_user(name=name, uid=uid, gid=gid, group=group, groups=groups, shell=shell, home=home) 50 | -------------------------------------------------------------------------------- /pysa/scanner/actions/utils.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-3-28 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Ken 21 | ''' 22 | 23 | import re 24 | import os 25 | import stat 26 | import pwd 27 | import grp 28 | import subprocess 29 | import logging 30 | import string 31 | 32 | 33 | PAT_IP = re.compile(r'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$', re.S) 34 | TEXT_CHARACTERS = "".join(map(chr, range(32, 127)) + list("\n\r\t\b")) 35 | NULL_TRANS = string.maketrans("", "") 36 | 37 | def lsb_release_codename(): 38 | """ 39 | Return the OS release's codename. 40 | """ 41 | if hasattr(lsb_release_codename, '_cache'): 42 | return lsb_release_codename._cache 43 | try: 44 | p = subprocess.Popen(['lsb_release', '-c'], stdout=subprocess.PIPE) 45 | except OSError: 46 | lsb_release_codename._cache = None 47 | return lsb_release_codename._cache 48 | stdout, stderr = p.communicate() 49 | if 0 != p.returncode: 50 | lsb_release_codename._cache = None 51 | return lsb_release_codename._cache 52 | match = re.search(r'\t(\w+)$', stdout) 53 | if match is None: 54 | lsb_release_codename._cache = None 55 | return lsb_release_codename._cache 56 | lsb_release_codename._cache = match.group(1) 57 | return lsb_release_codename._cache 58 | 59 | def rubygems_unversioned(): 60 | """ 61 | Determine whether RubyGems is suffixed by the Ruby language version. 62 | It ceased to be on Oneiric. It always has been on RPM-based distros. 63 | """ 64 | codename = lsb_release_codename() 65 | return codename is None or codename[0] >= 'o' 66 | 67 | def rubygems_virtual(): 68 | """ 69 | Determine whether RubyGems is baked into the Ruby 1.9 distribution. 70 | It is on Maverick and newer systems. 71 | """ 72 | codename = lsb_release_codename() 73 | return codename is not None and codename[0] >= 'm' 74 | 75 | def rubygems_path(): 76 | """ 77 | Determine based on the OS release where RubyGems will install gems. 78 | """ 79 | if lsb_release_codename() is None or rubygems_update(): 80 | return '/usr/lib/ruby/gems' 81 | return '/var/lib/gems' 82 | 83 | def mtime(pathname): 84 | try: 85 | return os.stat(pathname).st_mtime 86 | except OSError: 87 | return 0 88 | 89 | # open the cache file 90 | def open_cache(pathname, mode): 91 | f = open(pathname, mode) 92 | uid = int(os.environ['SUDO_UID']) 93 | gid = int(os.environ['SUDO_GID']) 94 | os.fchown(f.fileno(), uid, gid) 95 | return f 96 | 97 | def valid_txtfile(pathname): 98 | # only file 99 | if os.path.isdir(pathname)==True: 100 | return False 101 | 102 | # only plane text file 103 | ########################################### 104 | #cmd = '/usr/bin/file -bi ' + pathname 105 | #f = os.popen(cmd, 'r') 106 | #if f.read().startswith('text') == False: 107 | # return False 108 | ########################################### 109 | if istextfile(pathname)==0: 110 | return False 111 | 112 | # get the ctime; 113 | s = os.lstat(pathname) 114 | # And ignore block special files, character special files, 115 | # pipes, sockets and symbolic links. 116 | if stat.S_ISBLK(s.st_mode) \ 117 | or stat.S_ISCHR(s.st_mode) \ 118 | or stat.S_ISFIFO(s.st_mode) \ 119 | or stat.S_ISSOCK(s.st_mode) \ 120 | or stat.S_ISLNK(s.st_mode): 121 | return False 122 | 123 | return True 124 | 125 | def istextfile(filename, blocksize = 512): 126 | try: 127 | ret = istext(open(filename).read(blocksize)) 128 | except IOError: 129 | return 0 130 | return ret 131 | 132 | def istext(s): 133 | if "\0" in s: 134 | return 0 135 | 136 | if not s: # Empty files are considered text 137 | return 1 138 | 139 | # Get the non-text characters (maps a character to itself then 140 | # use the 'remove' option to get rid of the text characters.) 141 | t = s.translate(NULL_TRANS, TEXT_CHARACTERS) 142 | 143 | # If more than 30% non-text characters, then 144 | # this is considered a binary file 145 | if len(t)/len(s) > 0.30: 146 | return 0 147 | return 1 148 | 149 | def get_stat(pathname): 150 | try: 151 | s = os.lstat(pathname) 152 | pw = pwd.getpwuid(s.st_uid) 153 | owner = pw.pw_name 154 | 155 | gr = grp.getgrgid(s.st_gid) 156 | group = gr.gr_name 157 | 158 | mode = oct( s.st_mode & 0777 ) 159 | 160 | except KeyError: 161 | owner = s.st_uid 162 | group = s.st_gid 163 | mode = oct( 0777 ) 164 | 165 | return (group, mode, owner) 166 | 167 | def get_content(pathname): 168 | # read the config file's content 169 | try: 170 | content = open(pathname).read() 171 | return content 172 | except IOError, e: 173 | logging.error('utils.get_content(): Can not get file content, %s' % str(e)) 174 | return None 175 | 176 | def get_ips(filename): 177 | ips = [] 178 | try: 179 | file = open(filename, "r") 180 | 181 | # read through the file 182 | for line in file.readlines(): 183 | line = line.rstrip() 184 | 185 | regex = re.findall(r'[0-9]+(?:\.[0-9]+){3}', line) 186 | # if the regex is not empty and is not already in ips list append 187 | for ip in regex: 188 | if ip is not None and ip not in ips: 189 | if (PAT_IP.match(ip)) and (not ip.startswith('127.')) and (not ip.startswith('0.')): 190 | ips.append(ip) 191 | 192 | file.close() 193 | 194 | except IOError, (errno, strerror): 195 | return ips 196 | 197 | return ips 198 | -------------------------------------------------------------------------------- /pysa/scanner/object/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VisualOps/pysa/a3493826f5feb3ba59bed227fa397ce1c7916e9d/pysa/scanner/object/__init__.py -------------------------------------------------------------------------------- /pysa/scanner/object/cron.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-3-28 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Ken 21 | ''' 22 | 23 | from pysa.scanner.object.object_base import ObjectBase 24 | 25 | 26 | class Cron(ObjectBase): 27 | 28 | def __init__(self, name, command, minute, month, monthday, weekday, hour, target=None, user=None, environment=None): 29 | self.command = command 30 | self.environment = '/bin:/usr/bin:/usr/sbin:' + environment if environment else '/bin:/usr/bin:/usr/sbin' 31 | self.minute = minute 32 | self.month = month 33 | self.monthday = monthday 34 | self.name = name 35 | self.target = target 36 | self.user = user 37 | self.weekday = weekday 38 | self.hour = hour 39 | -------------------------------------------------------------------------------- /pysa/scanner/object/file.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-3-27 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Ken 21 | ''' 22 | 23 | from pysa.scanner.object.object_base import ObjectBase 24 | 25 | 26 | class File(ObjectBase): 27 | 28 | def __init__(self, checksum, content, group, mode, owner, path, force=False, provider=None, recurse=None, recurselimit=None, source=None): 29 | self.checksum = checksum 30 | self.content = content 31 | self.group = group 32 | self.mode = mode 33 | self.owner = owner 34 | self.path = path 35 | self.force = force 36 | self.provider = provider 37 | self.recurse = recurse 38 | self.recurselimit = recurselimit 39 | self.source = source 40 | 41 | -------------------------------------------------------------------------------- /pysa/scanner/object/group.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-3-28 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Ken 21 | ''' 22 | 23 | from pysa.scanner.object.object_base import ObjectBase 24 | 25 | 26 | class Group(ObjectBase): 27 | def __init__(self, name, gid, member=None): 28 | self.name = name 29 | self.gid = gid 30 | self.member = None 31 | -------------------------------------------------------------------------------- /pysa/scanner/object/host.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-3-28 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Ken 21 | ''' 22 | 23 | from pysa.scanner.object.object_base import ObjectBase 24 | 25 | 26 | class Host(ObjectBase): 27 | 28 | def __init__(self, ip, name, target, host_aliases=None): 29 | self.ip = ip 30 | self.name = name 31 | self.target = target 32 | self.host_aliases = host_aliases 33 | -------------------------------------------------------------------------------- /pysa/scanner/object/mount.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-3-28 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Ken 21 | ''' 22 | 23 | from pysa.scanner.object.object_base import ObjectBase 24 | 25 | 26 | class Mount(ObjectBase): 27 | 28 | def __init__(self, device, fstype, name, atboot=None, dump=None, remounts=None, options=None, size=None): 29 | self.device = device 30 | self.fstype = fstype 31 | self.name = name 32 | self.atboot = atboot 33 | self.dump = dump 34 | self.remounts = remounts 35 | self.options = options 36 | self.size = size 37 | -------------------------------------------------------------------------------- /pysa/scanner/object/object_base.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-3-27 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Ken 21 | ''' 22 | 23 | import re 24 | 25 | 26 | class ObjectBase(object): 27 | 28 | def prase(self): 29 | format_object = {} 30 | for attr in dir(self): 31 | if type(eval("self.%s" % attr)) in (str, int, dict, list, unicode) and not attr.startswith('_') and attr not in ['primarykey', 'primaryvalue']: 32 | format_object[attr] = eval("self.%s" % attr) 33 | return format_object 34 | 35 | @property 36 | def primarykey(self): 37 | pk = { 38 | 'Package' : 'name', 39 | 'File' : 'path', 40 | 'User' : 'name', 41 | 'Service' : 'name', 42 | 'Repository': 'path', 43 | 'Group' : 'name', 44 | 'Cron' : 'name', 45 | 'Host' : 'name', 46 | 'Mount' : 'device', 47 | 'SSHKey' : 'name', 48 | 'Source' : 'path', 49 | 'Process' : 'pid' 50 | } 51 | 52 | return pk.get(self.__class__.__name__) 53 | 54 | @property 55 | def primaryvalue(self): 56 | return getattr(self, self.primarykey) 57 | 58 | def attr_filter(self, attr_rules): 59 | """ 60 | attribute filter 61 | """ 62 | 63 | if not attr_rules: return False 64 | 65 | # ignore rule's case 66 | type_list = [ i for i in attr_rules.keys() if i.upper()==(self.__class__.__name__).upper() ] 67 | if not type_list: return False 68 | 69 | the_type = type_list[0] 70 | for (attr, rules) in attr_rules[the_type].items(): 71 | if not hasattr(self, attr): continue 72 | 73 | # get the object's attribute value 74 | value = getattr(self, attr) 75 | 76 | # ignore the null attribute value 77 | if value is None or not value: continue 78 | 79 | for rule in rules: 80 | if isinstance(value, list): # list value 81 | if all(isinstance(i, str) for i in value) or all(isinstance(i, unicode) for i in value): 82 | if len([ m.group(0) for i in value for m in [re.match("%s$"%rule, i)] if m ])>0: return True 83 | elif isinstance(value, str) or isinstance(value, unicode): # string 84 | if re.search(rule, value): return True 85 | return False 86 | 87 | def add_filter(self, add_rules): 88 | """ 89 | addition filter 90 | """ 91 | 92 | if not add_rules: return 93 | 94 | type_list = [ i for i in add_rules.keys() if i.upper()==(self.__class__.__name__).upper() ] 95 | if not type_list: return False 96 | 97 | the_type = type_list[0] 98 | for attr in add_rules[the_type]: 99 | # check whether has the attribute 100 | if not hasattr(self, attr): continue 101 | 102 | values = add_rules[the_type][attr] 103 | # global setting 104 | if isinstance(values, str) or isinstance(values, unicode): 105 | setattr(self, attr, values) 106 | elif isinstance(values, list): 107 | setattr(self, attr, values[0]) 108 | # single setting 109 | elif isinstance(values, dict): 110 | for value in values: 111 | if getattr(self, attr) == value: # check whether the object 112 | rules = values[value] 113 | # update 114 | for (add_attr, add_value) in rules.items(): 115 | if hasattr(self, add_attr) and len(add_value)>0: 116 | setattr(self, add_attr, add_value[0]) 117 | -------------------------------------------------------------------------------- /pysa/scanner/object/package.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-3-27 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Ken 21 | ''' 22 | 23 | from pysa.scanner.object.object_base import ObjectBase 24 | 25 | 26 | class Package(ObjectBase): 27 | 28 | def __init__(self, name, files=None, description=None, version=None, responsefile=None, provider=None, 29 | instance=None, category=None, platform=None, manager=None, root=None, vendor=None): 30 | self.name = name 31 | self.config_files = files 32 | self.description = description 33 | self.version = version 34 | self.responsefile = responsefile 35 | self.provider = provider 36 | self.instance = instance 37 | self.category = category 38 | self.platform = platform 39 | self.root = root 40 | self.manager = manager 41 | self.vendor = vendor 42 | -------------------------------------------------------------------------------- /pysa/scanner/object/process.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-04-19 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Michael 21 | ''' 22 | 23 | from pysa.scanner.object.object_base import ObjectBase 24 | 25 | 26 | class Process(ObjectBase): 27 | 28 | def __init__(self, pid, owner, status, cpu, mem, cmd, ppid=None): 29 | self.pid = pid # process id 30 | self.owner = owner 31 | self.status = status # D:uninterruptible sleep, R:runnable, S:sleeping, T:raced or stopped, Z:defunct process 32 | self.cpu = cpu # cpu% 33 | self.mem = mem # mem% 34 | self.cmd = cmd 35 | self.ppid = ppid # parent pid 36 | -------------------------------------------------------------------------------- /pysa/scanner/object/repository.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-04-18 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Michael 21 | ''' 22 | 23 | from pysa.scanner.object.object_base import ObjectBase 24 | 25 | 26 | class Repository(ObjectBase): 27 | 28 | def __init__(self, checksum, content, group, mode, owner, path, force=False, provider=None, recurse=None, recurselimit=None, source=None): 29 | self.checksum = checksum 30 | self.content = content 31 | self.group = group 32 | self.mode = mode 33 | self.owner = owner 34 | self.path = path 35 | self.force = force 36 | self.provider = provider 37 | self.recurse = recurse 38 | self.recurselimit = recurselimit 39 | self.source = source 40 | -------------------------------------------------------------------------------- /pysa/scanner/object/service.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-3-28 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Ken 21 | ''' 22 | 23 | from pysa.scanner.object.object_base import ObjectBase 24 | 25 | 26 | class Service(ObjectBase): 27 | 28 | def __init__(self, enable, hasrestart, name, path, \ 29 | provider=None, binary=None, control=None, ensure=None, \ 30 | hasstatus=None, manifest=None, start=None, stop=None, restart=None): 31 | self.enable = enable 32 | self.hasrestart = hasrestart 33 | self.name = name 34 | self.path = path 35 | self.provider = provider 36 | self.binary = binary 37 | self.control = control, 38 | self.ensure = ensure 39 | self.hasstatus = hasstatus 40 | self.manifest = manifest 41 | self.start = start 42 | self.stop = stop 43 | self.restart = restart 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /pysa/scanner/object/source.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-3-28 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Ken 21 | ''' 22 | 23 | from pysa.scanner.object.object_base import ObjectBase 24 | 25 | 26 | class Source(ObjectBase): 27 | 28 | def __init__(self, source, name, path, owner, group, mode, scm, branch, checksum=None, password=None, key=None): 29 | self.source = source 30 | self.path = path 31 | self.owner = owner 32 | self.name = name 33 | self.group = group 34 | self.mode = mode 35 | self.scm = scm 36 | self.branch = branch 37 | self.checksum = checksum 38 | self.password = password 39 | self.key = key 40 | -------------------------------------------------------------------------------- /pysa/scanner/object/sshkey.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-3-28 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Ken 21 | ''' 22 | 23 | from pysa.scanner.object.object_base import ObjectBase 24 | 25 | 26 | class SSHKey(ObjectBase): 27 | 28 | def __init__(self, key, name, path, mode, _type=None, target=None, host_aliases=None, user=None): 29 | self.key = key 30 | self.name = name 31 | self.target = target 32 | self.host_aliases = host_aliases 33 | self.type = _type 34 | self.user = user 35 | self.path = path 36 | self.mode = mode 37 | 38 | -------------------------------------------------------------------------------- /pysa/scanner/object/user.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-3-28 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Ken 21 | ''' 22 | 23 | from pysa.scanner.object.object_base import ObjectBase 24 | 25 | 26 | class User(ObjectBase): 27 | 28 | def __init__(self, name, uid, gid, group, groups=None, password=None, expiry=None, shell=None, home=None): 29 | self.name = name 30 | self.gid = gid 31 | self.group = group 32 | self.expiry = expiry 33 | self.groups = groups 34 | self.password = password 35 | self.uid = uid 36 | self.shell = shell 37 | self.home = home 38 | -------------------------------------------------------------------------------- /pysa/scanner/scanner_handler.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 2013-3-28 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Ken 21 | ''' 22 | #------------------------------------------------------------ 23 | import logging 24 | import time 25 | 26 | from pysa.scanner.actions.file import ScannerFile 27 | from pysa.scanner.actions.gem import ScannerGem 28 | from pysa.scanner.actions.npm import ScannerNpm 29 | from pysa.scanner.actions.php import ScannerPhp 30 | from pysa.scanner.actions.pypi import ScannerPypi 31 | from pysa.scanner.actions.service import ScannerService 32 | from pysa.scanner.actions.host import ScannerHost 33 | from pysa.scanner.actions.mount import ScannerMount 34 | from pysa.scanner.actions.cron import ScannerCron 35 | from pysa.scanner.actions.sshkey import ScannerKey 36 | from pysa.scanner.actions.user import ScannerUser 37 | from pysa.scanner.actions.group import ScannerGroup 38 | from pysa.scanner.actions.package import ScannerPackage 39 | from pysa.scanner.actions.source import ScannerSource 40 | from pysa.scanner.actions.repository import ScannerRepo 41 | from pysa.scanner.actions.process import ScannerProcess 42 | from pysa.scanner.actions.base import ScannerBase 43 | #------------------------------------------------------------ 44 | 45 | class ScannerHandler(): 46 | 47 | # stay aware of the order 48 | handler = { 49 | "file" : ScannerFile, 50 | "gem" : ScannerGem, 51 | "npm" : ScannerNpm, 52 | "php" : ScannerPhp, 53 | "pypi" : ScannerPypi, 54 | "service" : ScannerService, 55 | "host" : ScannerHost, 56 | 'user' : ScannerUser, 57 | 'group' : ScannerGroup, 58 | 'mount' : ScannerMount, 59 | 'cron' : ScannerCron, 60 | 'key' : ScannerKey, 61 | 'package' : ScannerPackage, 62 | 'source' : ScannerSource, 63 | 'repository': ScannerRepo, 64 | 'process' : ScannerProcess 65 | } 66 | 67 | 68 | 69 | def __init__(self, rules): 70 | self.resources = { 71 | 'packages' : {}, 72 | 'files' : {}, 73 | 'crons' : {}, 74 | 'groups' : {}, 75 | 'mounts' : {}, 76 | 'hosts' : {}, 77 | 'repos' : {}, 78 | 'services' : {}, 79 | 'keys' : {}, 80 | 'users' : {}, 81 | 'ips' : [], 82 | 'sources' : {}, 83 | 'proces' : {} 84 | 85 | } 86 | 87 | self.rules = rules if rules else {} 88 | 89 | def scan(self): 90 | 91 | # init the base scanner 92 | s = ScannerBase( 93 | self.resources['packages'], 94 | self.resources['files'], 95 | self.resources['crons'], 96 | self.resources['groups'], 97 | self.resources['mounts'], 98 | self.resources['hosts'], 99 | self.resources['repos'], 100 | self.resources['services'], 101 | self.resources['keys'], 102 | self.resources['users'], 103 | self.resources['ips'], 104 | self.resources['sources'], 105 | self.resources['proces'] 106 | ) 107 | # init the filter rules 108 | s.init_filter(self.rules) 109 | 110 | for scanner_key in self.handler.keys(): 111 | # ignore the discard resources 112 | if 'discard' in self.rules and '_resources' in self.rules['discard'] and scanner_key in self.rules['discard']['_resources']: continue 113 | 114 | # time begin 115 | time_begin = time.time() 116 | 117 | # log 118 | logging.info('ScannerHandler.scan(): Scanning Module %s, time begin at %s ' % (scanner_key, time_begin)) 119 | 120 | # scan according to different modules 121 | try: 122 | s.__class__ = self.handler[scanner_key] 123 | 124 | s.scan() 125 | 126 | except Exception, e: 127 | logging.error('ScannerHandler.scan(): %s Error message, %s' % (scanner_key,str(e))) 128 | 129 | # time end 130 | time_consume = time.time() - time_begin 131 | logging.info('ScannerHandler.scan(): Scanning Module %s, time consume %s ' % (scanner_key, time_consume)) 132 | 133 | return self.resources 134 | 135 | def module_scan(filters = None): 136 | scanner = ScannerHandler(filters) 137 | return scanner.scan() 138 | -------------------------------------------------------------------------------- /pysa/tools.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Common tools 3 | 4 | pysa - reverse a complete computer setup 5 | Copyright (C) 2013 MadeiraCloud Ltd. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | @author: Thibault BRONCHAIN 21 | ''' 22 | 23 | import os 24 | import os.path 25 | import logging 26 | import re 27 | import copy 28 | 29 | from pysa.exception import * 30 | 31 | 32 | INFO = "INFO" 33 | DEBUG = "DEBUG" 34 | ERR = "ERROR" 35 | 36 | LOGGING_EQ = { 37 | INFO : logging.info, 38 | DEBUG : logging.debug, 39 | ERR : logging.error 40 | } 41 | 42 | 43 | # common tools collection 44 | class Tools(): 45 | # logging 46 | @staticmethod 47 | def l(action, content, f, c = None): 48 | out = ("%s." % (c.__class__.__name__) if c else "") 49 | out += "%s()" % (f) 50 | LOGGING_EQ[action]("%s: %s" % (out, content)) 51 | 52 | # add a tab 53 | @staticmethod 54 | def tab_inc(tab): 55 | return tab+'\t' 56 | 57 | # delete a tab 58 | @staticmethod 59 | def tab_dec(tab): 60 | return tab[1:] 61 | 62 | # write data in a specific file 63 | @staticmethod 64 | def write_in_file(fname, content): 65 | Tools.l(INFO, "creating file %s" % (fname), 'write_in_file') 66 | dirname = os.path.dirname(fname) 67 | if not os.path.exists(dirname): 68 | os.makedirs(dirname) 69 | f = open(fname,'w') 70 | f.write(content) 71 | f.close() 72 | 73 | # get recursive path dirlist 74 | @staticmethod 75 | def get_recurse_path(path): 76 | rpath = re.split('/', path) 77 | i = 1 78 | while i < len(rpath): 79 | #DEBUG 80 | # rpath[i] = "%s/%s" % (rpath[i-1] if i else "", rpath[i]) 81 | #/DEBUG 82 | rpath[i] = os.path.normpath("%s/%s" % (rpath[i-1] if i else "", rpath[i])) 83 | i += 1 84 | rpath[0] = '/' 85 | return rpath 86 | 87 | # get previous path 88 | @staticmethod 89 | def path_basename(path): 90 | if path == '/': return None 91 | if path[-1] == '/': path = path[:-1] 92 | rpath = re.split('/', path) 93 | i = 1 94 | while i < len(rpath): 95 | # DEBUG 96 | # rpath[i] = "%s/%s" % (rpath[i-1] if i else "", rpath[i]) 97 | #/DEBUG 98 | rpath[i] = os.path.normpath("%s/%s" % (rpath[i-1] if i else "", rpath[i])) 99 | i += 1 100 | rpath[0] = '/' 101 | if len(rpath) < 2: return None 102 | return rpath[-2] 103 | 104 | # returns file content 105 | @staticmethod 106 | def get_file(filename): 107 | if not filename: return None 108 | file = None 109 | try: 110 | f = open(filename, 'r') 111 | file = f.read() 112 | except IOError: 113 | Tools.l(ERR, "%s: no such file or directory" % (filename), 'dump_file') 114 | return None 115 | return file 116 | 117 | # check if file exists 118 | @staticmethod 119 | def file_exists(filename): 120 | if not filename: return None 121 | try: 122 | with open(filename): pass 123 | except IOError: 124 | Tools.l(ERR, "%s: no such file or directory" % (filename), 'file_exists') 125 | return None 126 | return filename 127 | 128 | # merge lists recursive 129 | @staticmethod 130 | def list_merging(first, second): 131 | f = (first if first else []) 132 | s = (second if second else []) 133 | return f+s 134 | 135 | # remove childs after dict merging 136 | @staticmethod 137 | def dict_cleaner(input): 138 | output = {} 139 | for key in input: 140 | if type(input[key]) is not dict: 141 | output[key] = input[key] 142 | return output 143 | 144 | # ensure dictionary existency 145 | @staticmethod 146 | def s_dict_merging(first, second, duplicate = True): 147 | d = Tools.dict_merging(first,second, duplicate) 148 | return (d if d else {}) 149 | 150 | # merge dicts /!\ recursive 151 | @staticmethod 152 | def dict_merging(first, second, duplicate = True): 153 | if (not first) and (not second): 154 | return None 155 | elif not first: 156 | return (copy.deepcopy(second) if duplicate else second) 157 | elif not second: 158 | return (copy.deepcopy(first) if duplicate else first) 159 | repl = copy.deepcopy(first) 160 | for item in second: 161 | if (first.get(item)) and (type(first[item]) != type (second[item])): 162 | continue 163 | elif not first.get(item): 164 | repl[item] = second[item] 165 | elif type(second[item]) is dict: 166 | # recursion here 167 | val = Tools.dict_merging(first[item], second.get(item), True) 168 | if val != None: 169 | repl[item] = val 170 | elif type(second[item]) is list: 171 | repl[item] = first[item] + second[item] 172 | else: 173 | repl[item] = second[item] 174 | return repl 175 | 176 | # merge strings and lists 177 | @staticmethod 178 | def merge_string_list(first, second): 179 | if not first: return second 180 | elif not second: return first 181 | elif (type(first) is list) and (type(second) is list): return first+second 182 | elif (type(first) is list): 183 | first.append(second) 184 | return first 185 | elif (type(second) is list): 186 | second.append(first) 187 | return second 188 | else: return [first, second] 189 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | Installation script for setup from pip 4 | 5 | pysa - reverse a complete computer setup 6 | Copyright (C) 2013 MadeiraCloud Ltd. 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | @author: Thibault BRONCHAIN 22 | ''' 23 | 24 | import os 25 | import sys 26 | import shutil 27 | #import subprocess 28 | import setuptools 29 | 30 | from distutils.core import setup 31 | #from distutils.command.install import install 32 | #from distutils.command.sdist import sdist 33 | 34 | DISTNAME = 'Pysa' 35 | VERSION = '0.3b5' 36 | DESCRIPTION = 'Reverse your Servers Configuration' 37 | LONG_DESCRIPTION = open('README.txt').read() 38 | MAINTAINER = 'Thibault BRONCHAIN - MadeiraCloud Ltd.' 39 | MAINTAINER_EMAIL = 'pysa@mc2.io' 40 | LICENSE = 'LICENSE.txt' 41 | URL = 'http://madeiracloud.github.io/pysa/' 42 | DOWNLOAD_URL = 'http://pypi.python.org/packages/source/P/Pysa/Pysa-'+VERSION+'.tar.gz' 43 | #PACKAGES = ['pysa'] 44 | #PACKAGE_DIR = {'pysa': 'pysa'} 45 | SCRIPTS = ['bin/pysa2puppet', 'bin/pysa'] 46 | 47 | #def abspath(path): 48 | # return os.path.abspath(os.path.join(os.path.dirname(__file__), path)) 49 | # 50 | #class pysa_install(install): 51 | # user_options = install.user_options 52 | # 53 | # def initialize_options(self): 54 | # install.initialize_options(self) 55 | # 56 | # def run(self): 57 | # install.run(self) 58 | # 59 | # man_dir = abspath("./docs/man/") 60 | # output = subprocess.Popen( 61 | # [os.path.join(man_dir, "pysa_man.sh")], 62 | # stdout=subprocess.PIPE, 63 | # stderr=subprocess.PIPE, 64 | # cwd=man_dir, 65 | # env=dict({"PREFIX": self.prefix}, **dict(os.environ)) 66 | # ).communicate()[0] 67 | # print output 68 | # 69 | #class pysa_sdist(sdist): 70 | # def run(self): 71 | # sdist.run(self) 72 | 73 | if __name__ == "__main__": 74 | pkg = setuptools.find_packages() 75 | setup(name=DISTNAME, 76 | version=VERSION, 77 | author=MAINTAINER, 78 | author_email=MAINTAINER_EMAIL, 79 | maintainer=MAINTAINER, 80 | maintainer_email=MAINTAINER_EMAIL, 81 | url=URL, 82 | description=DESCRIPTION, 83 | long_description=LONG_DESCRIPTION, 84 | download_url=DOWNLOAD_URL, 85 | license=LICENSE, 86 | packages = pkg, 87 | # packages=PACKAGES, 88 | # package_dir=PACKAGE_DIR, 89 | scripts=SCRIPTS, 90 | classifiers = [ 91 | 'Development Status :: 4 - Beta', 92 | 'Environment :: Console', 93 | 'Intended Audience :: System Administrators', 94 | 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 95 | 'Operating System :: POSIX :: Linux', 96 | 'Topic :: System', 97 | ], 98 | # cmdclass={"install": pysa_install, "sdist": pysa_sdist}, 99 | ) 100 | --------------------------------------------------------------------------------