Browse Source

Merge pull request #8733 from mgugino-upstream-stage/ini-migrate

Migrate old master env files to new location
OpenShift Merge Robot 6 years ago
parent
commit
ff9f93c540

+ 234 - 0
roles/lib_utils/library/master_env_config_migrate.py

@@ -0,0 +1,234 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+#
+# Copyright 2018 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+try:
+    import configparser
+    CONFIG_PROXY_NEW = True
+except ImportError:
+    # configparser is available in python 2.7 backports, but that package
+    # might not be installed.
+    import ConfigParser as configparser
+    CONFIG_PROXY_NEW = False
+import sys
+import os
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+DOCUMENTATION = '''
+---
+module: master_env_config_migrate
+
+short_description: Migrates an environment file from one location to another.
+
+version_added: "2.4"
+
+description:
+    - Ensures that an environment file is properly migrated and values are properly
+      quoted.
+
+options:
+    src:
+        description:
+            - This is the original file on remote host.
+        required: true
+    dest:
+        description:
+            - This is the output location.
+        required: true
+
+author:
+    - "Michael Gugino <mgugino@redhat.com>"
+'''
+
+
+class SectionlessParser(configparser.RawConfigParser):
+    # pylint: disable=invalid-name,too-many-locals,too-many-branches,too-many-statements
+    # pylint: disable=anomalous-backslash-in-string,raising-bad-type
+    """RawConfigParser that allows no sections"""
+    # This code originally retrieved from:
+    # https://github.com/python/cpython/blob/master/Lib/configparser.py
+    # Copyright 2001-2018 Python Software Foundation; All Rights Reserved
+    # Modified to allow no sections.
+    def optionxform(self, optionstr):
+        """Override this method, don't set .lower()"""
+        return optionstr
+
+    def _write_section(self, fp, section_name, section_items, delimiter):
+        """Override for formatting"""
+        for key, value in section_items:
+            if " " in value and "\ " not in value and not value.startswith('"'):
+                value = u'"{}"'.format(value)
+            value = self._interpolation.before_write(self, section_name, key,
+                                                     value)
+            if value is not None or not self._allow_no_value:
+                value = delimiter + str(value).replace('\n', '\n\t')
+            else:
+                value = u""
+            fp.write(u"{}{}\n".format(key, value))
+            fp.write(u"\n")
+
+    def write(self, fp, space_around_delimiters=True):
+        """Ovrride write method"""
+        delimiters = ('=', ':')
+        if space_around_delimiters:
+            d = " {} ".format(delimiters[0])
+        else:
+            d = delimiters[0]
+        for section in self._sections:
+            self._write_section(fp, section,
+                                self._sections[section].items(), d)
+
+    def _set_proxies(self, sectname):
+        """set proxies"""
+        self._proxies[sectname] = configparser.SectionProxy(self, sectname)
+
+    def _read(self, fp, fpname):
+        """Parse a sectionless configuration file."""
+        elements_added = set()
+        cursect = {}
+        sectname = '__none_sect'
+        self._sections[sectname] = cursect
+        self._set_proxies(sectname)
+        optname = None
+        lineno = 0
+        indent_level = 0
+        e = None                              # None, or an exception
+        for lineno, line in enumerate(fp, start=1):
+            comment_start = sys.maxsize
+            # strip inline comments
+            inline_prefixes = {p: -1 for p in self._inline_comment_prefixes}
+            while comment_start == sys.maxsize and inline_prefixes:
+                next_prefixes = {}
+                for prefix, index in inline_prefixes.items():
+                    index = line.find(prefix, index + 1)
+                    if index == -1:
+                        continue
+                    next_prefixes[prefix] = index
+                    if index == 0 or (index > 0 and line[index - 1].isspace()):
+                        comment_start = min(comment_start, index)
+                inline_prefixes = next_prefixes
+            # strip full line comments
+            for prefix in self._comment_prefixes:
+                if line.strip().startswith(prefix):
+                    comment_start = 0
+                    break
+            if comment_start == sys.maxsize:
+                comment_start = None
+            value = line[:comment_start].strip()
+            if not value:
+                if self._empty_lines_in_values:
+                    # add empty line to the value, but only if there was no
+                    # comment on the line
+                    if (comment_start is None and
+                            cursect is not None and
+                            optname and
+                            cursect[optname] is not None):
+                        cursect[optname].append('')  # newlines added at join
+                else:
+                    # empty line marks end of value
+                    indent_level = sys.maxsize
+                continue
+            # continuation line?
+            first_nonspace = self.NONSPACECRE.search(line)
+            cur_indent_level = first_nonspace.start() if first_nonspace else 0
+            if (cursect is not None and optname and
+                    cur_indent_level > indent_level):
+                cursect[optname].append(value)
+
+            # a section header or option header?
+            else:
+                indent_level = cur_indent_level
+                # is it a section header?
+                mo = self.SECTCRE.match(value)
+                if mo:
+                    optname = None
+                else:
+                    mo = self._optcre.match(value)
+                    if mo:
+                        optname, _, optval = mo.group('option', 'vi', 'value')
+                        if not optname:
+                            e = self._handle_error(e, fpname, lineno, line)
+                        optname = self.optionxform(optname.rstrip())
+                        elements_added.add((sectname, optname))
+                        if optval is not None:
+                            optval = optval.strip()
+                            cursect[optname] = [optval]
+                        else:
+                            # valueless option handling
+                            cursect[optname] = None
+                    else:
+                        e = self._handle_error(e, fpname, lineno, line)
+        self._join_multiline_values()
+        # if any parsing errors occurred, raise an exception
+        if e:
+            raise e
+
+
+# pylint: disable=R0901,C0103,R0204
+class SectionlessParserOld(SectionlessParser):
+    """Overrides write method to utilize newer abstraction"""
+    def _set_proxies(self, sectname):
+        """proxies not present in old version"""
+        pass
+
+
+def create_file(src, dest):
+    '''Create the dest file from src file'''
+    if CONFIG_PROXY_NEW:
+        config = SectionlessParser()
+    else:
+        config = SectionlessParserOld()
+    config.readfp(open(src))
+    with open(dest, 'w') as output:
+        config.write(output, False)
+
+
+def run_module():
+    '''Run this module'''
+    module_args = dict(
+        src=dict(required=True, type='path'),
+        dest=dict(required=True, type='path'),
+    )
+
+    module = AnsibleModule(
+        argument_spec=module_args,
+        supports_check_mode=False
+    )
+
+    # First, create our dest dir if necessary
+    dest = module.params['dest']
+    src = module.params['src']
+
+    if os.path.exists(dest):
+        # Do nothing, output file already in place.
+        result = {'changed': False}
+        module.exit_json(**result)
+
+    create_file(src, dest)
+
+    result = {'changed': True}
+    module.exit_json(**result)
+
+
+def main():
+    run_module()
+
+
+if __name__ == '__main__':
+    main()

+ 67 - 0
roles/lib_utils/test/test_master_env_config_migrate.py

@@ -0,0 +1,67 @@
+import os
+import sys
+import io
+
+MODULE_PATH = os.path.realpath(os.path.join(__file__, os.pardir, os.pardir, 'library'))
+sys.path.insert(1, MODULE_PATH)
+
+import master_env_config_migrate  # noqa
+
+INFILE = u"""
+t1=Yes
+t2=A Space
+t3=An\ escaped
+t4="A quoted space"
+t5="a quoted \\
+  escaped line"
+t6 = an unquoted multiline \\
+  string
+"""
+
+
+def test_read_write_ini():
+    infile = io.StringIO(INFILE)
+
+    outfile = io.StringIO()
+
+    if master_env_config_migrate.CONFIG_PROXY_NEW:
+        config = master_env_config_migrate.SectionlessParser()
+    else:
+        config = master_env_config_migrate.SectionlessParserOld()
+
+    config.readfp(infile)
+    config.write(outfile, False)
+    print(outfile.getvalue())
+    # TODO(michaelgugino): Come up with some clever way to assert the file is
+    # correct.
+
+
+def test_read_write_ini_old():
+    master_env_config_migrate.CONFIG_PROXY_NEW = False
+    test_read_write_ini()
+
+# Contents for t.in:
+############################
+# t1=This is a spaced string
+# t2="Quoted spaced string"
+# t3=Escaped\ spaced\ string
+# t4 = String
+# t5="Quoted multiline string\
+#  with escaped newline"
+# t6=escaped\ spaced\\
+#  multiline\ unquoted.
+
+
+if __name__ == '__main__':
+    test_read_write_ini()
+    test_read_write_ini_old()
+    with open('t.in') as f:
+        config = master_env_config_migrate.SectionlessParser()
+        config.readfp(f)
+    with open('t.out', 'w') as f:
+        config.write(f, False)
+    with open('t.in') as f:
+        config = master_env_config_migrate.SectionlessParserOld()
+        config.readfp(f)
+    with open('t2.out', 'w') as f:
+        config.write(f, False)

+ 18 - 4
roles/openshift_control_plane/tasks/upgrade.yml

@@ -74,12 +74,26 @@
   - networkConfig.clusterNetworkCIDR
   - networkConfig.hostSubnetLength
 
+# TODO(michaelgugino): Remove in 3.11
+- name: Check for old master env file
+  stat:
+    path: "/etc/sysconfig/{{ openshift_service_type }}-master-api"
+  register: old_env
 
-- name: Create the master service env file if it does not exist
-  template:
-    src: "master.env.j2"
+# TODO(michaelgugino): Remove in 3.11
+- name: Migrate old master env file to new location
+  master_env_config_migrate:
+    src: "/etc/sysconfig/{{ openshift_service_type }}-master-api"
     dest: "{{ openshift.common.config_base }}/master/master.env"
-    force: no
+  when: old_env.stat.exists
+
+# TODO(michaelgugino): Remove in 3.11
+- name: mv old master env file to .backup
+  command: "mv {{ l_old_env_file }} {{ l_old_env_file_backup }}"
+  vars:
+    l_old_env_file: "/etc/sysconfig/{{ openshift_service_type }}-master-api"
+    l_old_env_file_backup: "/etc/sysconfig/{{ openshift_service_type }}-master-api.backup"
+  when: old_env.stat.exists
 
 - name: Update oreg value
   yedit: