github.com/greenboxal/deis@v1.12.1/contrib/linode/apply-firewall.py (about)

     1  #!/usr/bin/env python
     2  """
     3  Apply a "Security Group" to the members of an etcd cluster.
     4  
     5  Usage: apply-firewall.py
     6  """
     7  import os
     8  import re
     9  import string
    10  import argparse
    11  from threading import Thread
    12  import uuid
    13  
    14  import colorama
    15  from colorama import Fore, Style
    16  import paramiko
    17  import requests
    18  import sys
    19  import yaml
    20  
    21  
    22  def get_nodes_from_args(args):
    23      if args.discovery_url is not None:
    24          return get_nodes_from_discovery_url(args.discovery_url)
    25  
    26      return get_nodes_from_discovery_url(get_discovery_url_from_user_data())
    27  
    28  
    29  def get_nodes_from_discovery_url(discovery_url):
    30      try:
    31          nodes = []
    32          json = requests.get(discovery_url).json()
    33          discovery_nodes = json['node']['nodes']
    34          for node in discovery_nodes:
    35              value = node['value']
    36              ip = re.search('([0-9]{1,3}\.){3}[0-9]{1,3}', value).group(0)
    37              nodes.append(ip)
    38          return nodes
    39      except:
    40          raise IOError('Could not load nodes from discovery url ' + discovery_url)
    41  
    42  
    43  def get_discovery_url_from_user_data():
    44      name = 'linode-user-data.yaml'
    45      log_info('Loading discovery url from ' + name)
    46      try:
    47          current_dir = os.path.dirname(__file__)
    48          user_data_file = file(os.path.abspath(os.path.join(current_dir, name)), 'r')
    49          user_data_yaml = yaml.safe_load(user_data_file)
    50          return user_data_yaml['coreos']['etcd2']['discovery']
    51      except:
    52          raise IOError('Could not load discovery url from ' + name)
    53  
    54  
    55  def validate_ip_address(ip):
    56      return True if re.match('([0-9]{1,3}\.){3}[0-9]{1,3}', ip) else False
    57  
    58  
    59  def get_firewall_contents(node_ips, private=False):
    60      rules_template_text = """*filter
    61  :INPUT DROP [0:0]
    62  :FORWARD DROP [0:0]
    63  :OUTPUT ACCEPT [0:0]
    64  :DOCKER - [0:0]
    65  :Firewall-INPUT - [0:0]
    66  -A INPUT -j Firewall-INPUT
    67  -A FORWARD -j Firewall-INPUT
    68  -A Firewall-INPUT -i lo -j ACCEPT
    69  -A Firewall-INPUT -p icmp --icmp-type echo-reply -j ACCEPT
    70  -A Firewall-INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT
    71  -A Firewall-INPUT -p icmp --icmp-type time-exceeded -j ACCEPT
    72  # Ping
    73  -A Firewall-INPUT -p icmp --icmp-type echo-request -j ACCEPT
    74  # Accept any established connections
    75  -A Firewall-INPUT -m conntrack --ctstate  ESTABLISHED,RELATED -j ACCEPT
    76  # Enable the traffic between the nodes of the cluster
    77  -A Firewall-INPUT -s $node_ips -j ACCEPT
    78  # Allow connections from docker container
    79  -A Firewall-INPUT -i docker0 -j ACCEPT
    80  # Accept ssh, http, https and git
    81  -A Firewall-INPUT -m conntrack --ctstate NEW -m multiport$multiport_private -p tcp --dports 22,2222,80,443 -j ACCEPT
    82  # Log and drop everything else
    83  -A Firewall-INPUT -j REJECT
    84  COMMIT
    85  """
    86  
    87      multiport_private = ' -s 192.168.0.0/16' if private else ''
    88  
    89      rules_template = string.Template(rules_template_text)
    90      return rules_template.substitute(node_ips=string.join(node_ips, ','), multiport_private=multiport_private)
    91  
    92  
    93  def apply_rules_to_all(host_ips, rules, private_key):
    94      pkey = detect_and_create_private_key(private_key)
    95  
    96      threads = []
    97      for ip in host_ips:
    98          t = Thread(target=apply_rules, args=(ip, rules, pkey))
    99          t.setDaemon(False)
   100          t.start()
   101          threads.append(t)
   102      for thread in threads:
   103          thread.join()
   104  
   105  
   106  def detect_and_create_private_key(private_key):
   107      private_key_text = private_key.read()
   108      private_key.seek(0)
   109      if '-----BEGIN RSA PRIVATE KEY-----' in private_key_text:
   110          return paramiko.RSAKey.from_private_key(private_key)
   111      elif '-----BEGIN DSA PRIVATE KEY-----' in private_key_text:
   112          return paramiko.DSSKey.from_private_key(private_key)
   113      else:
   114          raise ValueError('Invalid private key file ' + private_key.name)
   115  
   116  
   117  def apply_rules(host_ip, rules, private_key):
   118      # connect to the server via ssh
   119      ssh = paramiko.SSHClient()
   120      ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
   121      ssh.connect(host_ip, username='core', allow_agent=False, look_for_keys=False, pkey=private_key)
   122  
   123      # copy the rules to the temp directory
   124      temp_file = '/tmp/' + str(uuid.uuid4())
   125  
   126      ssh.open_sftp()
   127      sftp = ssh.open_sftp()
   128      sftp.open(temp_file, 'w').write(rules)
   129  
   130      # move the rules in to place and enable and run the iptables-restore.service
   131      commands = [
   132          'sudo mv ' + temp_file + ' /var/lib/iptables/rules-save',
   133          'sudo chown root:root /var/lib/iptables/rules-save',
   134          'sudo systemctl enable iptables-restore.service',
   135          'sudo systemctl start iptables-restore.service'
   136      ]
   137  
   138      for command in commands:
   139          stdin, stdout, stderr = ssh.exec_command(command)
   140          stdout.channel.recv_exit_status()
   141  
   142      ssh.close()
   143  
   144      log_success('Applied rule to ' + host_ip)
   145  
   146  
   147  def main():
   148      colorama.init()
   149  
   150      parser = argparse.ArgumentParser(description='Apply a "Security Group" to a Deis cluster')
   151      parser.add_argument('--private-key', required=True, type=file, dest='private_key', help='Cluster SSH Private Key')
   152      parser.add_argument('--private', action='store_true', dest='private', help='Only allow access to the cluster from the private network')
   153      parser.add_argument('--discovery-url', dest='discovery_url', help='Etcd discovery url')
   154      parser.add_argument('--hosts', nargs='+', dest='hosts', help='The IP addresses of the hosts to apply rules to')
   155      args = parser.parse_args()
   156  
   157      nodes = get_nodes_from_args(args)
   158      hosts = args.hosts if args.hosts is not None else nodes
   159  
   160      node_ips = []
   161      for ip in nodes:
   162          if validate_ip_address(ip):
   163              node_ips.append(ip)
   164          else:
   165              log_warning('Invalid IP will not be added to security group: ' + ip)
   166  
   167      if not len(node_ips) > 0:
   168          raise ValueError('No valid IP addresses in security group.')
   169  
   170      host_ips = []
   171      for ip in hosts:
   172          if validate_ip_address(ip):
   173              host_ips.append(ip)
   174          else:
   175              log_warning('Host has invalid IP address: ' + ip)
   176  
   177      if not len(host_ips) > 0:
   178          raise ValueError('No valid host addresses.')
   179  
   180      log_info('Generating iptables rules...')
   181      rules = get_firewall_contents(node_ips, args.private)
   182      log_success('Generated rules:')
   183      log_debug(rules)
   184  
   185      log_info('Applying rules...')
   186      apply_rules_to_all(host_ips, rules, args.private_key)
   187      log_success('Done!')
   188  
   189  
   190  def log_debug(message):
   191      print(Style.DIM + Fore.MAGENTA + message + Fore.RESET + Style.RESET_ALL)
   192  
   193  
   194  def log_info(message):
   195      print(Fore.CYAN + message + Fore.RESET)
   196  
   197  
   198  def log_warning(message):
   199      print(Fore.YELLOW + message + Fore.RESET)
   200  
   201  
   202  def log_success(message):
   203      print(Style.BRIGHT + Fore.GREEN + message + Fore.RESET + Style.RESET_ALL)
   204  
   205  
   206  def log_error(message):
   207      print(Style.BRIGHT + Fore.RED + message + Fore.RESET + Style.RESET_ALL)
   208  
   209  if __name__ == "__main__":
   210      try:
   211          main()
   212      except Exception as e:
   213          log_error(e.message)
   214          sys.exit(1)