"Fossies" - the Fresh Open Source Software Archive

Member "eucalyptus-4.4.2/tools/authorize-migration-keys" (4 Aug 2017, 12598 Bytes) of package /linux/misc/eucalyptus-4.4.2.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "authorize-migration-keys": 4.4.1_vs_4.4.2.

    1 #!/usr/bin/python -tt
    2 
    3 #
    4 # (c) Copyright 2017 Hewlett Packard Enterprise Development Company LP
    5 #
    6 # This program is free software: you can redistribute it and/or modify
    7 # it under the terms of the GNU General Public License as published by
    8 # the Free Software Foundation; version 3 of the License.
    9 #
   10 # This program is distributed in the hope that it will be useful,
   11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
   12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   13 # GNU General Public License for more details.
   14 #
   15 # You should have received a copy of the GNU General Public License
   16 # along with this program.  If not, see http://www.gnu.org/licenses/.
   17 #
   18 
   19 import os
   20 import re
   21 import sys
   22 import time
   23 import shutil
   24 import logging
   25 import os.path
   26 import argparse
   27 import StringIO
   28 import subprocess
   29 from subprocess import CalledProcessError
   30 
   31 MAX_RETRIES = 20
   32 LIBVIRT_CONF = '/etc/libvirt/libvirtd.conf'
   33 KEYPATH = "{}/var/lib/eucalyptus/keys/node-cert.pem".format(os.getenv('EUCALYPTUS', ''))
   34 CERT_TOOL = ['certtool', '-i', '--infile', KEYPATH]
   35 DN_ORDERS = (('C', 'O', 'L', 'CN'),
   36              ('CN', 'O', 'L', 'C'))
   37 EMPTY_TLS_ALLOWED = 'tls_allowed_dn_list = []\n'
   38 TLS_ALLOWED_BEGIN = 'tls_allowed_dn_list = ['
   39 
   40 def getLogger(name, verbose):
   41     '''
   42     Setup simple logging to stdout
   43     '''
   44     logger = logging.getLogger(name)
   45     if verbose:
   46         logger.setLevel(logging.DEBUG)
   47     else:
   48         logger.setLevel(logging.INFO)
   49     ch = logging.StreamHandler()
   50     if verbose:
   51         ch.setLevel(logging.DEBUG)
   52     else:
   53         ch.setLevel(logging.INFO)
   54     formatter = logging.Formatter('%(asctime)s - %(message)s')
   55     ch.setFormatter(formatter)
   56     logger.addHandler(ch)
   57 
   58     return logger
   59 
   60 
   61 def parse_dn(dn):
   62     '''
   63     Parse a string in the form: 'CN=v1,O=v2,L=secret...' into a dictionary.
   64     strips off Subject: prefix if present.
   65     '''
   66     dn_split = dn.strip().replace('Subject: ', '').split(',')
   67     dn_dict = {i: j for i, j in [(k.split('=')) for k in dn_split]}
   68     return dn_dict
   69 
   70 
   71 def get_cert_subject():
   72     '''
   73     Read the certificate using certtool and get the subject line to
   74     parse out the components. Certificate errors are considered fatal
   75 
   76     certificate: /var/lib/eucalyptus/keys/node-cert.pem
   77 
   78     returns: Dictionary with component names as keys on success
   79              Empty dictionary on failure
   80     '''
   81     subject_dict = {}
   82 
   83     try:
   84         cert_output = subprocess.check_output(CERT_TOOL)
   85     except OSError as e:
   86         logger.error("Unable to execute command: %s error: %s:%s"
   87                      % (CERT_TOOL[0],e.errno, e.strerror))
   88         sys.exit(1)
   89     except subprocess.CalledProcessError as e:
   90         logger.error("Abnormal exit code from %s: returncode: %s"
   91                      % (e.cmd, e.returncode))
   92         sys.exit(1)
   93     except:
   94         logger.error("Unknown error encountered: %s"
   95                      % sys.exc_info()[0])
   96         sys.exit(1)
   97 
   98     subject = None
   99     for line in cert_output.split('\n'):
  100         if "Subject:" in line:
  101             subject = line
  102             break
  103 
  104     if subject is not None:
  105         subject_dict = parse_dn(subject)
  106 
  107     return subject_dict
  108 
  109 
  110 def generate_new_dn(client, secret):
  111     '''
  112     Given the client and secret passed, construct the appropriate
  113     Subject DNs to be used in the configuration file.
  114     '''
  115     new_dn = {'CN': client, 'L': secret}
  116 
  117     subject = get_cert_subject()
  118     if 'CN' in subject:
  119         new_dn['O'] = subject['CN']
  120     if 'C' in subject:
  121         new_dn['C'] = subject['C']
  122 
  123     # Make sure that we found an 'O' and 'C' attributes in the certificate
  124     if 'O' not in new_dn and 'C' not in new_dn:
  125         logger.error("Cannot construct new entry for %s %s exiting",
  126                      new_dn['CN'],
  127                      new_dn['L'])
  128         sys.exit(1)
  129 
  130     # multiple DNs are needed to accommodate different Linux release
  131     # ordering output from certtool
  132     new_dn_list = []
  133     for dn_order in DN_ORDERS:
  134         new_dn_list.append(','.join(["{}={}".format(i, new_dn[i]) for i in dn_order]))
  135     return new_dn_list
  136 
  137 def copy_file(src, dst):
  138     '''
  139     Copy a file, on failure exit program
  140     '''
  141     try:
  142         shutil.copy(src, dst)
  143     except IOError as e:
  144         logger.error("Unable to copy %s to %s errno: %d error: %s",
  145                      src, dst, e.errno, e.strerror)
  146         sys.exit(1)
  147 
  148 def move_file(src, dst):
  149     '''
  150     Move file, exit program on failure
  151     '''
  152     try:
  153         shutil.move(src, dst)
  154     except IOError as e:
  155         logger.error("Unable to move file %s to %s errno: %d error: %s",
  156                      src, dst, e.errno, e.strerror)
  157         sys.exit(1)
  158 
  159 def write_config(tls_allowed_dn_list, config_file):
  160     '''
  161     Modifies the configuration file on disk, in the default
  162     case (deauthorizing all clients) libvirtd will
  163     not be restarted if the list was originally empty.
  164 
  165     Creates a backup of the distribution configuration
  166     file to: config_file + '.orig', if not already present.
  167 
  168     If the file did not need to be modified, then no changes
  169     will be made to the file.
  170 
  171     returns: True - if the configuration file was modified.
  172              False - if there was no need to modify the configuration file
  173     '''
  174     updated = False
  175 
  176     config_file_orig = config_file + '.orig'
  177     config_file_bak = config_file + '.bak'
  178     config_file_new = config_file + '.new'
  179 
  180     if not os.path.exists(config_file):
  181         logger.error("Configuration file: %s is missing, exiting", config_file)
  182         sys.exit(1)
  183 
  184     # Copy distribution original to .orig
  185     if not os.path.exists(config_file_orig):
  186         copy_file(config_file, config_file_orig)
  187 
  188     # regex to pull the DNs within the buffer: ["dn1","dn2","dn3"]
  189     dn_regex = re.compile('"(.*?)"', re.MULTILINE)
  190 
  191     # buffer to run multiline regex against
  192     buffer = StringIO.StringIO()
  193 
  194     # Construct the list of CNs to possibly replace
  195     remove_dn_list = [i['CN'] for i in
  196                       [parse_dn(j) for j in tls_allowed_dn_list]]
  197 
  198     # Construct the new configuration file
  199     with open(config_file) as cf:
  200         try:
  201             with open(config_file_new, 'wc') as nf:
  202                 for line in cf:
  203                     if 'tls_allowed_dn_list' in line:
  204                         #
  205                         # Read the list of DNs into a buffer to parse
  206                         # Note the call to cf.next()
  207                         #
  208                         line_is_commented = line.strip().startswith('#')
  209 
  210                         buffer.write(line)
  211                         if ']' not in line:
  212                             # Read lines until we get the end bracket
  213                             while True:
  214                                 l = cf.next()
  215                                 buffer.write(l)
  216                                 if ']' in l:
  217                                     break
  218 
  219                         # Construct our current list of DNs
  220                         DN_list = dn_regex.findall(buffer.getvalue())
  221 
  222                         # Current list is empty, see if it is commented out or not
  223                         if len(DN_list) == 0 and len(tls_allowed_dn_list) == 0:
  224                             if line_is_commented:
  225                                 nf.write(EMPTY_TLS_ALLOWED)
  226                                 updated = True
  227                             else:
  228                                 break  # No change needed to the configuration file
  229                         elif len(tls_allowed_dn_list) == 0:
  230                             nf.write(EMPTY_TLS_ALLOWED)
  231                             updated = True
  232                         else:
  233                             # Read each current dn, if it matches what we are trying to add, we'll replace
  234                             # it. Otherwise, we'll add onto the current list
  235                             if line_is_commented:
  236                                 # commented config entry,
  237                                 # need to disregard the contents
  238                                 DN_list = []
  239 
  240                             new_dn_list = tls_allowed_dn_list[:]
  241 
  242                             for dn in DN_list:
  243                                 dn_dict = parse_dn(dn)
  244                                 if dn_dict['CN'] not in remove_dn_list:
  245                                     new_dn_list.append(dn)
  246 
  247                             #
  248                             # Write out the new list of dns
  249                             #
  250                             nf.write(TLS_ALLOWED_BEGIN)
  251                             padding = len(TLS_ALLOWED_BEGIN)
  252 
  253                             for i, dn in enumerate(new_dn_list):
  254                                 nf.write('"{}"'.format(dn))
  255 
  256                                 # Append comma, except at end of list, add padding on new line
  257                                 if i < len(new_dn_list)-1:
  258                                     nf.write(",\n")
  259                                     nf.write(" " * padding)
  260                             else:
  261                                 nf.write("]\n")
  262                             updated = True
  263                     else:
  264                         nf.write(line)
  265         except IOError as e:
  266             logger.error("Unable to write to new file: %s [Errno:%d] %s",
  267                          config_file_new,
  268                          e.errno,
  269                          e.strerror)
  270             sys.exit(1)
  271 
  272     if updated:
  273         # Copy current config to .bak
  274         copy_file(config_file, config_file_bak)
  275 
  276         # move .new file to real conf file
  277         move_file(config_file_new, config_file)
  278 
  279     elif os.path.exists(config_file_new):
  280         # No differences, remove the generated config file
  281         os.unlink(config_file_new)
  282     return updated
  283 
  284 if __name__ == '__main__':
  285 
  286     tls_allowed_dn_list = []
  287     config_updated = False
  288 
  289     parser = argparse.ArgumentParser(description='Modify migration keys for libvirtd')
  290     parser.add_argument('-v', action='store_true', dest='verbose',
  291                         help='Verbose')
  292     parser.add_argument('-r', action='store_true', dest='restart',
  293                         help='Restart libvirtd, if file changes were made')
  294     parser.add_argument('-c', metavar='filename', dest='config',
  295                         default=LIBVIRT_CONF,
  296                         help="Configuration file, default: %s" % LIBVIRT_CONF)
  297 
  298     group = parser.add_mutually_exclusive_group(required=True)
  299     group.add_argument('-a', nargs=2, metavar=('client', 'secret'),
  300                        help='Authorize client')
  301     group.add_argument('-D', action='store_true',
  302                        help='Deauthorize all migration clients')
  303 
  304     # Handle case where NC is calling with extra parameters that we don't need.
  305     parser.add_argument('args', metavar='', nargs=argparse.REMAINDER,
  306                         help=argparse.SUPPRESS) 
  307     args = parser.parse_args()
  308 
  309     logger = getLogger('authorize-migration-keys', args.verbose)
  310 
  311     if not os.path.exists(args.config):
  312         logger.error("Error, configuration file: %s not found, exiting", args.config)
  313         sys.exit(1)
  314     #
  315     # If called with -D then we need to clear the tls_allowed_dn_list.
  316     # If called with '-a client secret' then we need to *add*
  317     # to the tls_allowed_dn_list or replace current entries.
  318     #
  319     if not args.D:
  320         tls_allowed_dn_list = generate_new_dn(args.a[0], args.a[1])
  321 
  322     config_updated = write_config(tls_allowed_dn_list, args.config)
  323 
  324     if args.restart and config_updated:
  325         logger.debug("Restarting libvirtd")
  326         subprocess.call(['/usr/bin/systemctl', 'restart', 'libvirtd.service'])
  327 
  328         #
  329         # After restarting libvirtd, we need to wait for it to become available so
  330         # that the node controller can connect with the hypervisor in startup
  331         # situations.
  332         #
  333         connected = False
  334         for i in range(0,MAX_RETRIES):
  335             try:
  336                 subprocess.check_call(['virsh','connect'],stdin=open('/dev/null'), stdout=open('/dev/null','w'))
  337                 connected = True
  338                 break
  339             except CalledProcessError as e:
  340                 logger.debug("Unable to connect to hypervisor on attempt number: [%d]", i+1)
  341                 time.sleep(1)
  342         if not connected:
  343             logger.error("Error, unable to connect to hypervisor after %d seconds",MAX_RETRIES)
  344             sys.exit(1)
  345     elif not config_updated:
  346         logger.debug("No configuration change, will not restart libvirtd")
  347     else:
  348         logger.debug("Configuration file changed, libvirtd restart not requested")
  349