diff --git a/sgmanager/__init__.py b/sgmanager/__init__.py index 1c84f85..80da605 100644 --- a/sgmanager/__init__.py +++ b/sgmanager/__init__.py @@ -18,6 +18,7 @@ class ThresholdException(Exception): friendly = True pass + class SGManager(object): def __init__(self, ec2_connection=None, vpc=False, only_groups=[]): """ @@ -27,16 +28,19 @@ def __init__(self, ec2_connection=None, vpc=False, only_groups=[]): """ global ec2 - if not ec2_connection: + if isinstance(ec2_connection, boto.ec2.connection.EC2Connection): # Use supplied connection + ec2 = ec2_connection + elif ec2_connection: + # Try to connect on our own try: ec2 = boto.connect_ec2() except boto.exception.NoAuthHandlerFound as e: e.friendly = True raise else: - # Try to connect on our own - ec2 = ec2_connection + # Continue without connection + ec2 = None if vpc: lg.debug("Working only with VPC security groups") @@ -89,6 +93,7 @@ def load_local_groups(self, config, mode): """ self.local = SecurityGroups(vpc=self.vpc, only_groups=self.only_groups) self.local.load_local_groups(config, mode) + self.local.check_validity() return self.local def dump_remote_groups(self): diff --git a/sgmanager/cli.py b/sgmanager/cli.py index 313bdf6..7dda1e6 100644 --- a/sgmanager/cli.py +++ b/sgmanager/cli.py @@ -10,6 +10,7 @@ import boto from sgmanager import SGManager +from sgmanager.exceptions import InvalidConfiguration import sgmanager.logger lg_root = sgmanager.logger.init(name='', syslog=False) @@ -38,7 +39,7 @@ def cli(): Main CLI entrance """ parser = argparse.ArgumentParser(description='Security groups management tool') - parser.add_argument('-c', '--config', help='Config file to use') + parser.add_argument('-c', '--config', help='Config file to use', required=True) parser.add_argument('--vpc', action='store_true', help='Work with VPC groups, otherwise only non-VPC') parser.add_argument('--dump', action='store_true', help='Dump remote groups and exit') parser.add_argument('--unused', action='store_true', help='Dump groups not used by any instance') @@ -58,6 +59,7 @@ def cli(): parser.add_argument('--insecure', action='store_true', help='Do not validate SSL certs') parser.add_argument('--threshold', help='Maximum threshold to use for add/rm of groups/rules in percentage (default: 15)', default=15) parser.add_argument('--cert', help='Path to CA certificates (eg. /etc/pki/cacert.pem)') + parser.add_argument('--validate', action='store_true', help='Dry-run, validates the config file') args = parser.parse_args() if args.quiet: @@ -71,9 +73,36 @@ def cli(): lg.setLevel(logging.DEBUG) lg_root.setLevel(logging.DEBUG) + mode = None + if args.mode in ('a', 'ascii'): + mode = 'ascii' + elif args.mode in ('s', 'strict'): + mode = 'strict' + elif args.mode in ('v', 'vpc') or args.vpc: + mode = 'vpc' + + if not mode: + lg.error('Invalid mode "%s" selected' % args.mode) + sys.exit(1) + # Initialize SGManager - ec2 = connect_ec2(args) + if not args.validate: + ec2 = connect_ec2(args) + else: + ec2 = None + args.only_groups = None + manager = SGManager(ec2, vpc=args.vpc, only_groups=args.only_groups) + + try: + manager.load_local_groups(args.config, mode) + except InvalidConfiguration as e: + lg.error("Invalid config file: %s" %e) + sys.exit(1) + + if args.validate: + sys.exit(0) + manager.load_remote_groups() if args.dump: @@ -91,24 +120,6 @@ def cli(): manager.remove_unused_groups(dry=not args.force) sys.exit(0) - if not args.config: - lg.error('No config file supplied') - sys.exit(1) - - mode = False - if args.mode in ('a', 'ascii'): - mode = 'ascii' - if args.mode in ('s', 'strict'): - mode = 'strict' - if args.mode in ('v', 'vpc') or args.vpc: - mode = 'vpc' - - if not mode: - lg.error('Invalid mode "%s" selected' % args.mode) - sys.exit(1) - - manager.load_local_groups(args.config, mode) - # Parameters for manager.apply_diff() params = { 'dry' : not args.force, diff --git a/sgmanager/securitygroups/__init__.py b/sgmanager/securitygroups/__init__.py index 9437130..8a15f56 100644 --- a/sgmanager/securitygroups/__init__.py +++ b/sgmanager/securitygroups/__init__.py @@ -31,11 +31,14 @@ def __init__(self, vpc=False, only_groups=[]): self.groups = {} self.config = None - try: - self.owner_id = ec2.get_all_security_groups('default')[0].owner_id - lg.debug("Default owner id: %s" % self.owner_id) - except Exception as e: - lg.error("Can't load default security group to lookup owner id: %s" % e) + self.owner_id = None + if ec2: + try: + self.owner_id = ec2.get_all_security_groups('default')[0].owner_id + lg.debug("Default owner id: %s" % self.owner_id) + except Exception as e: + lg.error("Can't load default security group to lookup" + "owner id: %s" % e) def load_remote_groups(self): """ @@ -133,15 +136,19 @@ def load_local_groups(self, config, mode): raise InvalidConfiguration("Can't parse config file %s: error at line %s, column %s" % (config, mark.line+1, mark.column+1)) else: raise InvalidConfiguration("Can't parse config file %s: %s" % (config, e)) + # Empty config file is considered invalid + if not conf: + raise InvalidConfiguration("Config file %s is empty" % config) # Remove include keys conf = self._fix_include(conf) lg.debug("Loading local groups") - for name, group in conf.iteritems(): - if not self.only_groups or name in self.only_groups: - # Initialize SGroup object - self.groups[name] = self._load_sgroup(name, group, check_mode=mode) + if isinstance(conf, dict): + for name, group in conf.iteritems(): + if not self.only_groups or name in self.only_groups: + # Initialize SGroup object + self.groups[name] = self._load_sgroup(name, group, check_mode=mode) return self.groups @@ -365,6 +372,26 @@ def compare(self, other): return added, removed, updated, unchanged + def check_validity(self): + """ + Checks if groups mentioned in the rules are defined + and if ip address is in the correct format (including mask) + """ + for name, ref in self.groups.iteritems(): + for rule in ref.rules: + for group in rule.groups: + if not self.has_group(group['name']): + raise InvalidConfiguration("Group %s referenced by " + "group %s does not exist in the config file" + % (group['name'], name)) + if rule.cidr: + for ip in rule.cidr: + if not re.match('^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.' + '([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.' + '([01]?\\d\\d?|2[0-4]\\d|25[0-5])\/(\\d|[1-2]\\d|3[0-2])$', ip): + raise InvalidConfiguration("Wrong format of ip address '%s' " + "in the config file" % ip) + class YamlDumper(yaml.SafeDumper): """