diff options
39 files changed, 1736 insertions, 392 deletions
diff --git a/.tito/packages/openshift-ansible b/.tito/packages/openshift-ansible index 391d5bf9e..3ad40eefa 100644 --- a/.tito/packages/openshift-ansible +++ b/.tito/packages/openshift-ansible @@ -1 +1 @@ -3.0.69-1 ./ +3.0.72-1 ./ diff --git a/openshift-ansible.spec b/openshift-ansible.spec index b01ee6562..269422801 100644 --- a/openshift-ansible.spec +++ b/openshift-ansible.spec @@ -5,7 +5,7 @@ } Name: openshift-ansible -Version: 3.0.69 +Version: 3.0.72 Release: 1%{?dist} Summary: Openshift and Atomic Enterprise Ansible License: ASL 2.0 @@ -279,6 +279,27 @@ Atomic OpenShift Utilities includes %changelog +* Tue Apr 05 2016 Troy Dawson <tdawson@redhat.com> 3.0.72-1 +- when docker is installed, make it 1.8.2 to avoid issues (mwoodson@redhat.com) +- Downgrade to docker 1.8.2 if installing OSE < 3.2 (sdodson@redhat.com) +- Pacemaker is unsupported for 3.2 (bleanhar@redhat.com) +- Fixing regexp. Periods are no longer allowed (kwoodson@redhat.com) +- We require docker 1.9 for the 3.2 upgrade (bleanhar@redhat.com) + +* Mon Apr 04 2016 Troy Dawson <tdawson@redhat.com> 3.0.71-1 +- Fixed oc_edit by requiring name and content (kwoodson@redhat.com) +- add higher severity trigger if no heartbeat for 1 hour (jdiaz@redhat.com) +- Yedit enhancements (kwoodson@redhat.com) + +* Fri Apr 01 2016 Brenton Leanhardt <bleanhar@redhat.com> 3.0.70-1 +- Enable Ansible ssh pipelining to speedup deployment (lhuard@amadeus.com) +- Allow for overriding scheduler config (jdetiber@redhat.com) +- a-o-i: Add 3.2 to list of supported versions (smunilla@redhat.com) +- a-o-i: Support for unattended upgrades (smunilla@redhat.com) +- a-o-i: More flexible upgrade mappings (smunilla@redhat.com) +- a-o-i: OSE/AEP 3.2 product option (smunilla@redhat.com) +- a-o-i: Error out early if callback_facts is None (smunilla@redhat.com) + * Thu Mar 31 2016 Brenton Leanhardt <bleanhar@redhat.com> 3.0.69-1 - Bug 1320829 - Ensure docker installed for facts (jdetiber@redhat.com) - Bug 1322788 - The IMAGE_VERSION wasn't added to atomic-openshift-master-api diff --git a/playbooks/common/openshift-cluster/upgrades/v3_1_to_v3_2/pre.yml b/playbooks/common/openshift-cluster/upgrades/v3_1_to_v3_2/pre.yml index 6804de992..dd938f058 100644 --- a/playbooks/common/openshift-cluster/upgrades/v3_1_to_v3_2/pre.yml +++ b/playbooks/common/openshift-cluster/upgrades/v3_1_to_v3_2/pre.yml @@ -25,6 +25,7 @@ hosts: oo_first_master vars: target_version: "{{ '1.2' if deployment_type == 'origin' else '3.1.1.900' }}" + g_pacemaker_upgrade_url_segment: "{{ 'org/latest' if deployment_type =='origin' else '.com/enterprise/3.1' }}" gather_facts: no tasks: - fail: @@ -35,6 +36,12 @@ - fail: msg: > + This upgrade does not support Pacemaker: + https://docs.openshift.{{ g_pacemaker_upgrade_url_segment }}/install_config/upgrading/pacemaker_to_native_ha.html + when: openshift.master.cluster_method == 'pacemaker' + + - fail: + msg: > openshift_pkg_version is {{ openshift_pkg_version }} which is not a valid version for a {{ target_version }} upgrade when: openshift_pkg_version is defined and openshift_pkg_version.split('-',1).1 | version_compare(target_version ,'<') @@ -204,8 +211,7 @@ - fail: msg: This playbook requires access to Docker 1.9 or later - when: not openshift.common.is_atomic | bool - and (g_docker_version.avail_version | default(g_docker_version.curr_version, true) | version_compare('1.9','<')) + when: g_docker_version.avail_version | default(g_docker_version.curr_version, true) | version_compare('1.9','<') # TODO: add check to upgrade ostree to get latest Docker diff --git a/roles/docker/defaults/main.yml b/roles/docker/defaults/main.yml new file mode 100644 index 000000000..1b26af0dd --- /dev/null +++ b/roles/docker/defaults/main.yml @@ -0,0 +1,2 @@ +--- +docker_version: ''
\ No newline at end of file diff --git a/roles/docker/tasks/main.yml b/roles/docker/tasks/main.yml index 9709c5014..9cf949d65 100644 --- a/roles/docker/tasks/main.yml +++ b/roles/docker/tasks/main.yml @@ -1,32 +1,29 @@ --- # tasks file for docker -# Avoid docker 1.9 when installing origin < 1.2 or OSE < 3.2 on RHEL/Centos and -# See: https://bugzilla.redhat.com/show_bug.cgi?id=1304038 +- name: Get current installed version if docker_version is specified + command: "{{ repoquery_cmd }} --installed --qf '%{version}' docker" + when: not openshift.common.is_atomic | bool and docker_version != '' + register: docker_version_result + changed_when: false -- name: Default to latest docker for 1.2/3.2 or Fedora - set_fact: - docker_version: '' - when: openshift.common.version_gte_3_2_or_1_2 | bool or ansible_distribution == 'Fedora' +- name: Downgrade docker if necessary + command: "{{ ansible_pkg_mgr }} downgrade -y docker-{{ docker_version }}" + register: docker_downgrade_result + when: not docker_version_result | skipped and docker_version_result | default('0.0', True) | version_compare(docker_version, 'gt') -- name: Gather latest version of docker - shell: > - yum list available -e 0 -q "docker" 2>&1 | tail -n +2 | awk '{ print $2 }' | sort -r | tr '\n' ' ' | tail -n 1 - register: latest_docker - when: not openshift.common.version_gte_3_2_or_1_2 | bool and ansible_distribution != 'Fedora' - -- name: Check if Docker 1.9 is the latest - set_fact: - docker19_is_latest: "{{ True if '1.9' in latest_docker.stdout else False }}" - when: not openshift.common.version_gte_3_2_or_1_2 | bool and ansible_distribution != 'Fedora' +- name: Install docker + action: "{{ ansible_pkg_mgr }} name=docker{{ '-' + docker_version if docker_version != '' else '' }} state=present" + when: not openshift.common.is_atomic | bool and not docker_downgrade_result | changed -- set_fact: - docker_version: "{{ '-1.8.2' if docker19_is_latest | bool else ''}}" - when: not openshift.common.version_gte_3_2_or_1_2 | bool and ansible_distribution != 'Fedora' +- stat: path=/etc/sysconfig/docker + register: docker_check + when: docker_downgrade_result | changed -- name: Install docker - action: "{{ ansible_pkg_mgr }} name=docker{{ docker_version }} state=present" - when: not openshift.common.is_atomic | bool +- name: Remove deferred deletion for downgrades from 1.9 + command: > + sed -i 's/--storage-opt dm.use_deferred_deletion=true//' /etc/sysconfig/docker-storage + when: docker_downgrade_result | changed and docker_check.stat.exists | bool and docker_version_result | default('0.0', True) | version_compare('1.9', '>=') and docker_version | version_compare('1.9', '<') - name: enable and start the docker service service: diff --git a/roles/docker/vars/main.yml b/roles/docker/vars/main.yml index 162487545..606cdb9b9 100644 --- a/roles/docker/vars/main.yml +++ b/roles/docker/vars/main.yml @@ -1,3 +1,3 @@ --- - +repoquery_cmd: "{{ 'dnf repoquery --latest-limit 1 -d 0' if ansible_pkg_mgr == 'dnf' else 'repoquery' }}" udevw_udevd_dir: /etc/systemd/system/systemd-udevd.service.d diff --git a/roles/docker_storage_setup/tasks/main.yml b/roles/docker_storage_setup/tasks/main.yml index 8af9bbe1e..21c80e36e 100755 --- a/roles/docker_storage_setup/tasks/main.yml +++ b/roles/docker_storage_setup/tasks/main.yml @@ -2,7 +2,7 @@ - name: Ensure docker is installed action: "{{ ansible_pkg_mgr }} name={{ item }} state=present" with_items: - - docker + - docker-1.8.2 # Docker doesn't seem to start cleanly the first time run # when loopback (not directlvm) is configured. Putting in an diff --git a/roles/lib_openshift_api/build/ansible/edit.py b/roles/lib_openshift_api/build/ansible/edit.py new file mode 100644 index 000000000..943fa47a6 --- /dev/null +++ b/roles/lib_openshift_api/build/ansible/edit.py @@ -0,0 +1,84 @@ +# pylint: skip-file + +def main(): + ''' + ansible oc module for services + ''' + + module = AnsibleModule( + argument_spec=dict( + kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'), + state=dict(default='present', type='str', + choices=['present']), + debug=dict(default=False, type='bool'), + namespace=dict(default='default', type='str'), + name=dict(default=None, required=True, type='str'), + kind=dict(required=True, + type='str', + choices=['dc', 'deploymentconfig', + 'svc', 'service', + 'scc', 'securitycontextconstraints', + 'ns', 'namespace', 'project', 'projects', + 'is', 'imagestream', + 'istag', 'imagestreamtag', + 'bc', 'buildconfig', + 'routes', + 'node', + 'secret', + ]), + file_name=dict(default=None, type='str'), + file_format=dict(default='yaml', type='str'), + content=dict(default=None, required=True, type='dict'), + force=dict(default=False, type='bool'), + ), + supports_check_mode=True, + ) + ocedit = Edit(module.params['kind'], + module.params['namespace'], + module.params['name'], + kubeconfig=module.params['kubeconfig'], + verbose=module.params['debug']) + + state = module.params['state'] + + api_rval = ocedit.get() + + ######## + # Create + ######## + if not Utils.exists(api_rval['results'], module.params['name']): + module.fail_json(msg=api_rval) + + ######## + # Update + ######## + api_rval = ocedit.update(module.params['file_name'], + module.params['content'], + module.params['force'], + module.params['file_format']) + + + if api_rval['returncode'] != 0: + module.fail_json(msg=api_rval) + + if api_rval.has_key('updated') and not api_rval['updated']: + module.exit_json(changed=False, results=api_rval, state="present") + + # return the created object + api_rval = ocedit.get() + + if api_rval['returncode'] != 0: + module.fail_json(msg=api_rval) + + module.exit_json(changed=True, results=api_rval, state="present") + + module.exit_json(failed=True, + changed=False, + results='Unknown state passed. %s' % state, + state="unknown") + +# pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import, locally-disabled +# import module snippets. This are required +from ansible.module_utils.basic import * + +main() diff --git a/roles/lib_openshift_api/build/ansible/obj.py b/roles/lib_openshift_api/build/ansible/obj.py index 0796d807e..a14ac0e43 100644 --- a/roles/lib_openshift_api/build/ansible/obj.py +++ b/roles/lib_openshift_api/build/ansible/obj.py @@ -19,6 +19,13 @@ def main(): type='str', choices=['dc', 'deploymentconfig', 'svc', 'service', + 'scc', 'securitycontextconstraints', + 'ns', 'namespace', 'project', 'projects', + 'is', 'imagestream', + 'istag', 'imagestreamtag', + 'bc', 'buildconfig', + 'routes', + 'node', 'secret', ]), delete_after=dict(default=False, type='bool'), diff --git a/roles/lib_openshift_api/build/generate.py b/roles/lib_openshift_api/build/generate.py index 877ca1766..cf3f61d2c 100755 --- a/roles/lib_openshift_api/build/generate.py +++ b/roles/lib_openshift_api/build/generate.py @@ -15,6 +15,7 @@ GEN_STR = "#!/usr/bin/env python\n" + \ "# | |) | (_) | | .` | (_) || | | _|| |) | | | |\n" + \ "# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|\n" +OPENSHIFT_ANSIBLE_PATH = os.path.dirname(os.path.realpath(__file__)) FILES = {'oc_obj.py': ['src/base.py', @@ -27,18 +28,23 @@ FILES = {'oc_obj.py': ['src/base.py', 'src/secret.py', 'ansible/secret.py', ], + 'oc_edit.py': ['src/base.py', + '../../lib_yaml_editor/build/src/yedit.py', + 'src/edit.py', + 'ansible/edit.py', + ], } def main(): ''' combine the necessary files to create the ansible module ''' - openshift_ansible = ('../library/') + library = os.path.join(OPENSHIFT_ANSIBLE_PATH, '..', 'library/') for fname, parts in FILES.items(): - with open(os.path.join(openshift_ansible, fname), 'w') as afd: + with open(os.path.join(library, fname), 'w') as afd: afd.seek(0) afd.write(GEN_STR) for fpart in parts: - with open(fpart) as pfd: + with open(os.path.join(OPENSHIFT_ANSIBLE_PATH, fpart)) as pfd: # first line is pylint disable so skip it for idx, line in enumerate(pfd): if idx == 0 and 'skip-file' in line: diff --git a/roles/lib_openshift_api/build/src/base.py b/roles/lib_openshift_api/build/src/base.py index 31c102e5d..66831c4e2 100644 --- a/roles/lib_openshift_api/build/src/base.py +++ b/roles/lib_openshift_api/build/src/base.py @@ -8,7 +8,15 @@ import json import os import shutil import subprocess +import re + import yaml +# This is here because of a bug that causes yaml +# to incorrectly handle timezone info on timestamps +def timestamp_constructor(_, node): + '''return timestamps as strings''' + return str(node.value) +yaml.add_constructor(u'tag:yaml.org,2002:timestamp', timestamp_constructor) # pylint: disable=too-few-public-methods class OpenShiftCLI(object): @@ -32,8 +40,14 @@ class OpenShiftCLI(object): fname = '/tmp/%s' % rname yed = Yedit(fname, res['results'][0]) + changes = [] for key, value in content.items(): - yed.put(key, value) + changes.append(yed.put(key, value)) + + if any([not change[0] for change in changes]): + return {'returncode': 0, 'updated': False} + + yed.write() atexit.register(Utils.cleanup, [fname]) @@ -76,7 +90,9 @@ class OpenShiftCLI(object): cmds = ['/usr/bin/oc'] cmds.extend(cmd) + rval = {} results = '' + err = None if self.verbose: print ' '.join(cmds) @@ -85,27 +101,42 @@ class OpenShiftCLI(object): stdout=subprocess.PIPE, stderr=subprocess.PIPE, env={'KUBECONFIG': self.kubeconfig}) + proc.wait() + stdout = proc.stdout.read() + stderr = proc.stderr.read() + + rval = {"returncode": proc.returncode, + "results": results, + } + if proc.returncode == 0: if output: try: - results = json.loads(proc.stdout.read()) + rval['results'] = json.loads(stdout) except ValueError as err: if "No JSON object could be decoded" in err.message: - results = err.message + err = err.message if self.verbose: - print proc.stderr.read() - print results + print stdout + print stderr print - return {"returncode": proc.returncode, "results": results} + if err: + rval.update({"err": err, + "stderr": stderr, + "stdout": stdout, + "cmd": cmds + }) - return {"returncode": proc.returncode, - "stderr": proc.stderr.read(), - "stdout": proc.stdout.read(), - "results": {} - } + else: + rval.update({"stderr": stderr, + "stdout": stdout, + "results": {}, + }) + + return rval class Utils(object): ''' utilities for openshiftcli modules ''' @@ -179,7 +210,7 @@ class Utils(object): contents = sfd.read() if sfile_type == 'yaml': - contents = yaml.load(contents) + contents = yaml.safe_load(contents) elif sfile_type == 'json': contents = json.loads(contents) diff --git a/roles/lib_openshift_api/build/src/edit.py b/roles/lib_openshift_api/build/src/edit.py new file mode 100644 index 000000000..7020ace47 --- /dev/null +++ b/roles/lib_openshift_api/build/src/edit.py @@ -0,0 +1,49 @@ +# pylint: skip-file + +class Edit(OpenShiftCLI): + ''' Class to wrap the oc command line tools + ''' + # pylint: disable=too-many-arguments + def __init__(self, + kind, + namespace, + resource_name=None, + kubeconfig='/etc/origin/master/admin.kubeconfig', + verbose=False): + ''' Constructor for OpenshiftOC ''' + super(Edit, self).__init__(namespace, kubeconfig) + self.namespace = namespace + self.kind = kind + self.name = resource_name + self.kubeconfig = kubeconfig + self.verbose = verbose + + def get(self): + '''return a secret by name ''' + return self._get(self.kind, self.name) + + def update(self, file_name, content, force=False, content_type='yaml'): + '''run update ''' + if file_name: + if content_type == 'yaml': + data = yaml.load(open(file_name)) + elif content_type == 'json': + data = json.loads(open(file_name).read()) + + changes = [] + yed = Yedit(file_name, data) + for key, value in content.items(): + changes.append(yed.put(key, value)) + + if any([not change[0] for change in changes]): + return {'returncode': 0, 'updated': False} + + yed.write() + + atexit.register(Utils.cleanup, [file_name]) + + return self._replace(file_name, force=force) + + return self._replace_content(self.kind, self.name, content, force=force) + + diff --git a/roles/lib_openshift_api/build/test/edit.yml b/roles/lib_openshift_api/build/test/edit.yml new file mode 100755 index 000000000..9aa01303a --- /dev/null +++ b/roles/lib_openshift_api/build/test/edit.yml @@ -0,0 +1,53 @@ +#!/usr/bin/ansible-playbook +--- +- hosts: "oo_clusterid_mwoodson:&oo_version_3:&oo_master_primary" + gather_facts: no + user: root + + post_tasks: + - copy: + dest: "/tmp/{{ item }}" + src: "files/{{ item }}" + with_items: + - dc.yml + + - name: present dc + oc_edit: + kind: dc + namespace: default + name: router + content: + spec.template.spec.containers[0].ports[0].containerPort: 80 + spec.template.spec.containers[0].ports[0].hostPort: 80 + register: dcout + + - debug: + var: dcout + + - name: present dc + oc_edit: + kind: dc + namespace: default + name: router + content: + spec.template.spec.containers[0].ports[0].containerPort: 81 + spec.template.spec.containers[0].ports[0].hostPort: 81 + file_format: yaml + register: dcout + + - debug: + var: dcout + + - name: present dc + oc_edit: + kind: dc + namespace: default + name: router + content: + spec.template.spec.containers[0].ports[0].containerPort: 80 + spec.template.spec.containers[0].ports[0].hostPort: 80 + file_format: yaml + register: dcout + + - debug: + var: dcout diff --git a/roles/lib_openshift_api/build/test/files/dc.yml b/roles/lib_openshift_api/build/test/files/dc.yml index 7992c90dd..24f690ef4 100644 --- a/roles/lib_openshift_api/build/test/files/dc.yml +++ b/roles/lib_openshift_api/build/test/files/dc.yml @@ -1,14 +1,14 @@ apiVersion: v1 kind: DeploymentConfig metadata: - creationTimestamp: 2016-03-18T19:47:45Z + creationTimestamp: 2016-04-01T15:23:29Z labels: router: router name: router namespace: default - resourceVersion: "84016" + resourceVersion: "1338477" selfLink: /oapi/v1/namespaces/default/deploymentconfigs/router - uid: 48f8b9d9-ed42-11e5-9903-0a9a9d4e7f2b + uid: b00c7eba-f81d-11e5-809b-0a581f893e3f spec: replicas: 2 selector: @@ -117,5 +117,4 @@ status: details: causes: - type: ConfigChange - latestVersion: 1 - + latestVersion: 12 diff --git a/roles/lib_openshift_api/library/oc_edit.py b/roles/lib_openshift_api/library/oc_edit.py new file mode 100644 index 000000000..44e77331d --- /dev/null +++ b/roles/lib_openshift_api/library/oc_edit.py @@ -0,0 +1,619 @@ +#!/usr/bin/env python +# ___ ___ _ _ ___ ___ _ _____ ___ ___ +# / __| __| \| | __| _ \ /_\_ _| __| \ +# | (_ | _|| .` | _|| / / _ \| | | _|| |) | +# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____ +# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _| +# | |) | (_) | | .` | (_) || | | _|| |) | | | | +# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_| +''' + OpenShiftCLI class that wraps the oc commands in a subprocess +''' + +import atexit +import json +import os +import shutil +import subprocess +import re + +import yaml +# This is here because of a bug that causes yaml +# to incorrectly handle timezone info on timestamps +def timestamp_constructor(_, node): + '''return timestamps as strings''' + return str(node.value) +yaml.add_constructor(u'tag:yaml.org,2002:timestamp', timestamp_constructor) + +# pylint: disable=too-few-public-methods +class OpenShiftCLI(object): + ''' Class to wrap the oc command line tools ''' + def __init__(self, + namespace, + kubeconfig='/etc/origin/master/admin.kubeconfig', + verbose=False): + ''' Constructor for OpenshiftOC ''' + self.namespace = namespace + self.verbose = verbose + self.kubeconfig = kubeconfig + + # Pylint allows only 5 arguments to be passed. + # pylint: disable=too-many-arguments + def _replace_content(self, resource, rname, content, force=False): + ''' replace the current object with the content ''' + res = self._get(resource, rname) + if not res['results']: + return res + + fname = '/tmp/%s' % rname + yed = Yedit(fname, res['results'][0]) + changes = [] + for key, value in content.items(): + changes.append(yed.put(key, value)) + + if any([not change[0] for change in changes]): + return {'returncode': 0, 'updated': False} + + yed.write() + + atexit.register(Utils.cleanup, [fname]) + + return self._replace(fname, force) + + def _replace(self, fname, force=False): + '''return all pods ''' + cmd = ['-n', self.namespace, 'replace', '-f', fname] + if force: + cmd.append('--force') + return self.oc_cmd(cmd) + + def _create(self, fname): + '''return all pods ''' + return self.oc_cmd(['create', '-f', fname, '-n', self.namespace]) + + def _delete(self, resource, rname): + '''return all pods ''' + return self.oc_cmd(['delete', resource, rname, '-n', self.namespace]) + + def _get(self, resource, rname=None): + '''return a secret by name ''' + cmd = ['get', resource, '-o', 'json', '-n', self.namespace] + if rname: + cmd.append(rname) + + rval = self.oc_cmd(cmd, output=True) + + # Ensure results are retuned in an array + if rval.has_key('items'): + rval['results'] = rval['items'] + elif not isinstance(rval['results'], list): + rval['results'] = [rval['results']] + + return rval + + def oc_cmd(self, cmd, output=False): + '''Base command for oc ''' + #cmds = ['/usr/bin/oc', '--config', self.kubeconfig] + cmds = ['/usr/bin/oc'] + cmds.extend(cmd) + + rval = {} + results = '' + err = None + + if self.verbose: + print ' '.join(cmds) + + proc = subprocess.Popen(cmds, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env={'KUBECONFIG': self.kubeconfig}) + + proc.wait() + stdout = proc.stdout.read() + stderr = proc.stderr.read() + + rval = {"returncode": proc.returncode, + "results": results, + } + + if proc.returncode == 0: + if output: + try: + rval['results'] = json.loads(stdout) + except ValueError as err: + if "No JSON object could be decoded" in err.message: + err = err.message + + if self.verbose: + print stdout + print stderr + print + + if err: + rval.update({"err": err, + "stderr": stderr, + "stdout": stdout, + "cmd": cmds + }) + + else: + rval.update({"stderr": stderr, + "stdout": stdout, + "results": {}, + }) + + return rval + +class Utils(object): + ''' utilities for openshiftcli modules ''' + @staticmethod + def create_file(rname, data, ftype=None): + ''' create a file in tmp with name and contents''' + path = os.path.join('/tmp', rname) + with open(path, 'w') as fds: + if ftype == 'yaml': + fds.write(yaml.safe_dump(data, default_flow_style=False)) + + elif ftype == 'json': + fds.write(json.dumps(data)) + else: + fds.write(data) + + # Register cleanup when module is done + atexit.register(Utils.cleanup, [path]) + return path + + @staticmethod + def create_files_from_contents(data): + '''Turn an array of dict: filename, content into a files array''' + files = [] + + for sfile in data: + path = Utils.create_file(sfile['path'], sfile['content']) + files.append(path) + + return files + + @staticmethod + def cleanup(files): + '''Clean up on exit ''' + for sfile in files: + if os.path.exists(sfile): + if os.path.isdir(sfile): + shutil.rmtree(sfile) + elif os.path.isfile(sfile): + os.remove(sfile) + + + @staticmethod + def exists(results, _name): + ''' Check to see if the results include the name ''' + if not results: + return False + + + if Utils.find_result(results, _name): + return True + + return False + + @staticmethod + def find_result(results, _name): + ''' Find the specified result by name''' + rval = None + for result in results: + if result.has_key('metadata') and result['metadata']['name'] == _name: + rval = result + break + + return rval + + @staticmethod + def get_resource_file(sfile, sfile_type='yaml'): + ''' return the service file ''' + contents = None + with open(sfile) as sfd: + contents = sfd.read() + + if sfile_type == 'yaml': + contents = yaml.safe_load(contents) + elif sfile_type == 'json': + contents = json.loads(contents) + + return contents + + # Disabling too-many-branches. This is a yaml dictionary comparison function + # pylint: disable=too-many-branches,too-many-return-statements + @staticmethod + def check_def_equal(user_def, result_def, debug=False): + ''' Given a user defined definition, compare it with the results given back by our query. ''' + + # Currently these values are autogenerated and we do not need to check them + skip = ['metadata', 'status'] + + for key, value in result_def.items(): + if key in skip: + continue + + # Both are lists + if isinstance(value, list): + if not isinstance(user_def[key], list): + return False + + # lists should be identical + if value != user_def[key]: + return False + + # recurse on a dictionary + elif isinstance(value, dict): + if not isinstance(user_def[key], dict): + if debug: + print "dict returned false not instance of dict" + return False + + # before passing ensure keys match + api_values = set(value.keys()) - set(skip) + user_values = set(user_def[key].keys()) - set(skip) + if api_values != user_values: + if debug: + print api_values + print user_values + print "keys are not equal in dict" + return False + + result = Utils.check_def_equal(user_def[key], value, debug=debug) + if not result: + if debug: + print "dict returned false" + return False + + # Verify each key, value pair is the same + else: + if not user_def.has_key(key) or value != user_def[key]: + if debug: + print "value not equal; user_def does not have key" + print value + print user_def[key] + return False + + return True + +class YeditException(Exception): + ''' Exception class for Yedit ''' + pass + +class Yedit(object): + ''' Class to modify yaml files ''' + re_valid_key = r"(((\[-?\d+\])|([a-zA-Z-./]+)).?)+$" + re_key = r"(?:\[(-?\d+)\])|([a-zA-Z-./]+)" + + def __init__(self, filename=None, content=None, content_type='yaml'): + self.content = content + self.filename = filename + self.__yaml_dict = content + self.content_type = content_type + if self.filename and not self.content: + self.load(content_type=self.content_type) + + @property + def yaml_dict(self): + ''' getter method for yaml_dict ''' + return self.__yaml_dict + + @yaml_dict.setter + def yaml_dict(self, value): + ''' setter method for yaml_dict ''' + self.__yaml_dict = value + + @staticmethod + def remove_entry(data, key): + ''' remove data at location key ''' + if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))): + return None + + key_indexes = re.findall(Yedit.re_key, key) + for arr_ind, dict_key in key_indexes[:-1]: + if dict_key and isinstance(data, dict): + data = data.get(dict_key, None) + elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1: + data = data[int(arr_ind)] + else: + return None + + # process last index for remove + # expected list entry + if key_indexes[-1][0]: + if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: + del data[int(key_indexes[-1][0])] + return True + + # expected dict entry + elif key_indexes[-1][1]: + if isinstance(data, dict): + del data[key_indexes[-1][1]] + return True + + @staticmethod + def add_entry(data, key, item=None): + ''' Get an item from a dictionary with key notation a.b.c + d = {'a': {'b': 'c'}}} + key = a.b + return c + ''' + if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))): + return None + + curr_data = data + + key_indexes = re.findall(Yedit.re_key, key) + for arr_ind, dict_key in key_indexes[:-1]: + if dict_key: + if isinstance(data, dict) and data.has_key(dict_key): + data = data[dict_key] + continue + + data[dict_key] = {} + data = data[dict_key] + + elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1: + data = data[int(arr_ind)] + else: + return None + + # process last index for add + # expected list entry + if key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: + data[int(key_indexes[-1][0])] = item + + # expected dict entry + elif key_indexes[-1][1] and isinstance(data, dict): + data[key_indexes[-1][1]] = item + + return curr_data + + @staticmethod + def get_entry(data, key): + ''' Get an item from a dictionary with key notation a.b.c + d = {'a': {'b': 'c'}}} + key = a.b + return c + ''' + if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))): + return None + + key_indexes = re.findall(Yedit.re_key, key) + for arr_ind, dict_key in key_indexes: + if dict_key and isinstance(data, dict): + data = data.get(dict_key, None) + elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1: + data = data[int(arr_ind)] + else: + return None + + return data + + def write(self): + ''' write to file ''' + if not self.filename: + raise YeditException('Please specify a filename.') + + with open(self.filename, 'w') as yfd: + yfd.write(yaml.safe_dump(self.yaml_dict, default_flow_style=False)) + + def read(self): + ''' write to file ''' + # check if it exists + if not self.exists(): + return None + + contents = None + with open(self.filename) as yfd: + contents = yfd.read() + + return contents + + def exists(self): + ''' return whether file exists ''' + if os.path.exists(self.filename): + return True + + return False + + def load(self, content_type='yaml'): + ''' return yaml file ''' + contents = self.read() + + if not contents: + return None + + # check if it is yaml + try: + if content_type == 'yaml': + self.yaml_dict = yaml.load(contents) + elif content_type == 'json': + self.yaml_dict = json.loads(contents) + except yaml.YAMLError as _: + # Error loading yaml or json + return None + + return self.yaml_dict + + def get(self, key): + ''' get a specified key''' + try: + entry = Yedit.get_entry(self.yaml_dict, key) + except KeyError as _: + entry = None + + return entry + + def delete(self, key): + ''' remove key from a dict''' + try: + entry = Yedit.get_entry(self.yaml_dict, key) + except KeyError as _: + entry = None + if not entry: + return (False, self.yaml_dict) + + result = Yedit.remove_entry(self.yaml_dict, key) + if not result: + return (False, self.yaml_dict) + + return (True, self.yaml_dict) + + def put(self, key, value): + ''' put key, value into a dict ''' + try: + entry = Yedit.get_entry(self.yaml_dict, key) + except KeyError as _: + entry = None + + if entry == value: + return (False, self.yaml_dict) + + result = Yedit.add_entry(self.yaml_dict, key, value) + if not result: + return (False, self.yaml_dict) + + return (True, self.yaml_dict) + + def create(self, key, value): + ''' create a yaml file ''' + if not self.exists(): + self.yaml_dict = {key: value} + return (True, self.yaml_dict) + + return (False, self.yaml_dict) + +class Edit(OpenShiftCLI): + ''' Class to wrap the oc command line tools + ''' + # pylint: disable=too-many-arguments + def __init__(self, + kind, + namespace, + resource_name=None, + kubeconfig='/etc/origin/master/admin.kubeconfig', + verbose=False): + ''' Constructor for OpenshiftOC ''' + super(Edit, self).__init__(namespace, kubeconfig) + self.namespace = namespace + self.kind = kind + self.name = resource_name + self.kubeconfig = kubeconfig + self.verbose = verbose + + def get(self): + '''return a secret by name ''' + return self._get(self.kind, self.name) + + def update(self, file_name, content, force=False, content_type='yaml'): + '''run update ''' + if file_name: + if content_type == 'yaml': + data = yaml.load(open(file_name)) + elif content_type == 'json': + data = json.loads(open(file_name).read()) + + changes = [] + yed = Yedit(file_name, data) + for key, value in content.items(): + changes.append(yed.put(key, value)) + + if any([not change[0] for change in changes]): + return {'returncode': 0, 'updated': False} + + yed.write() + + atexit.register(Utils.cleanup, [file_name]) + + return self._replace(file_name, force=force) + + return self._replace_content(self.kind, self.name, content, force=force) + + + +def main(): + ''' + ansible oc module for services + ''' + + module = AnsibleModule( + argument_spec=dict( + kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'), + state=dict(default='present', type='str', + choices=['present']), + debug=dict(default=False, type='bool'), + namespace=dict(default='default', type='str'), + name=dict(default=None, required=True, type='str'), + kind=dict(required=True, + type='str', + choices=['dc', 'deploymentconfig', + 'svc', 'service', + 'scc', 'securitycontextconstraints', + 'ns', 'namespace', 'project', 'projects', + 'is', 'imagestream', + 'istag', 'imagestreamtag', + 'bc', 'buildconfig', + 'routes', + 'node', + 'secret', + ]), + file_name=dict(default=None, type='str'), + file_format=dict(default='yaml', type='str'), + content=dict(default=None, required=True, type='dict'), + force=dict(default=False, type='bool'), + ), + supports_check_mode=True, + ) + ocedit = Edit(module.params['kind'], + module.params['namespace'], + module.params['name'], + kubeconfig=module.params['kubeconfig'], + verbose=module.params['debug']) + + state = module.params['state'] + + api_rval = ocedit.get() + + ######## + # Create + ######## + if not Utils.exists(api_rval['results'], module.params['name']): + module.fail_json(msg=api_rval) + + ######## + # Update + ######## + api_rval = ocedit.update(module.params['file_name'], + module.params['content'], + module.params['force'], + module.params['file_format']) + + + if api_rval['returncode'] != 0: + module.fail_json(msg=api_rval) + + if api_rval.has_key('updated') and not api_rval['updated']: + module.exit_json(changed=False, results=api_rval, state="present") + + # return the created object + api_rval = ocedit.get() + + if api_rval['returncode'] != 0: + module.fail_json(msg=api_rval) + + module.exit_json(changed=True, results=api_rval, state="present") + + module.exit_json(failed=True, + changed=False, + results='Unknown state passed. %s' % state, + state="unknown") + +# pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import, locally-disabled +# import module snippets. This are required +from ansible.module_utils.basic import * + +main() diff --git a/roles/lib_openshift_api/library/oc_obj.py b/roles/lib_openshift_api/library/oc_obj.py index fa31416c0..c058072e3 100644 --- a/roles/lib_openshift_api/library/oc_obj.py +++ b/roles/lib_openshift_api/library/oc_obj.py @@ -15,7 +15,15 @@ import json import os import shutil import subprocess +import re + import yaml +# This is here because of a bug that causes yaml +# to incorrectly handle timezone info on timestamps +def timestamp_constructor(_, node): + '''return timestamps as strings''' + return str(node.value) +yaml.add_constructor(u'tag:yaml.org,2002:timestamp', timestamp_constructor) # pylint: disable=too-few-public-methods class OpenShiftCLI(object): @@ -39,8 +47,14 @@ class OpenShiftCLI(object): fname = '/tmp/%s' % rname yed = Yedit(fname, res['results'][0]) + changes = [] for key, value in content.items(): - yed.put(key, value) + changes.append(yed.put(key, value)) + + if any([not change[0] for change in changes]): + return {'returncode': 0, 'updated': False} + + yed.write() atexit.register(Utils.cleanup, [fname]) @@ -83,7 +97,9 @@ class OpenShiftCLI(object): cmds = ['/usr/bin/oc'] cmds.extend(cmd) + rval = {} results = '' + err = None if self.verbose: print ' '.join(cmds) @@ -92,27 +108,42 @@ class OpenShiftCLI(object): stdout=subprocess.PIPE, stderr=subprocess.PIPE, env={'KUBECONFIG': self.kubeconfig}) + proc.wait() + stdout = proc.stdout.read() + stderr = proc.stderr.read() + + rval = {"returncode": proc.returncode, + "results": results, + } + if proc.returncode == 0: if output: try: - results = json.loads(proc.stdout.read()) + rval['results'] = json.loads(stdout) except ValueError as err: if "No JSON object could be decoded" in err.message: - results = err.message + err = err.message if self.verbose: - print proc.stderr.read() - print results + print stdout + print stderr print - return {"returncode": proc.returncode, "results": results} + if err: + rval.update({"err": err, + "stderr": stderr, + "stdout": stdout, + "cmd": cmds + }) - return {"returncode": proc.returncode, - "stderr": proc.stderr.read(), - "stdout": proc.stdout.read(), - "results": {} - } + else: + rval.update({"stderr": stderr, + "stdout": stdout, + "results": {}, + }) + + return rval class Utils(object): ''' utilities for openshiftcli modules ''' @@ -186,7 +217,7 @@ class Utils(object): contents = sfd.read() if sfile_type == 'yaml': - contents = yaml.load(contents) + contents = yaml.safe_load(contents) elif sfile_type == 'json': contents = json.loads(contents) @@ -254,15 +285,16 @@ class YeditException(Exception): class Yedit(object): ''' Class to modify yaml files ''' + re_valid_key = r"(((\[-?\d+\])|([a-zA-Z-./]+)).?)+$" + re_key = r"(?:\[(-?\d+)\])|([a-zA-Z-./]+)" - def __init__(self, filename=None, content=None): + def __init__(self, filename=None, content=None, content_type='yaml'): self.content = content self.filename = filename self.__yaml_dict = content + self.content_type = content_type if self.filename and not self.content: - self.get() - elif self.filename and self.content: - self.write() + self.load(content_type=self.content_type) @property def yaml_dict(self): @@ -275,58 +307,91 @@ class Yedit(object): self.__yaml_dict = value @staticmethod - def remove_entry(data, keys): - ''' remove an item from a dictionary with key notation a.b.c - d = {'a': {'b': 'c'}}} - keys = a.b - item = c - ''' - if "." in keys: - key, rest = keys.split(".", 1) - if key in data.keys(): - Yedit.remove_entry(data[key], rest) - else: - del data[keys] + def remove_entry(data, key): + ''' remove data at location key ''' + if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))): + return None + + key_indexes = re.findall(Yedit.re_key, key) + for arr_ind, dict_key in key_indexes[:-1]: + if dict_key and isinstance(data, dict): + data = data.get(dict_key, None) + elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1: + data = data[int(arr_ind)] + else: + return None + + # process last index for remove + # expected list entry + if key_indexes[-1][0]: + if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: + del data[int(key_indexes[-1][0])] + return True + + # expected dict entry + elif key_indexes[-1][1]: + if isinstance(data, dict): + del data[key_indexes[-1][1]] + return True @staticmethod - def add_entry(data, keys, item): - ''' Add an item to a dictionary with key notation a.b.c + def add_entry(data, key, item=None): + ''' Get an item from a dictionary with key notation a.b.c d = {'a': {'b': 'c'}}} - keys = a.b - item = c + key = a.b + return c ''' - if "." in keys: - key, rest = keys.split(".", 1) - if key not in data: - data[key] = {} + if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))): + return None + + curr_data = data + + key_indexes = re.findall(Yedit.re_key, key) + for arr_ind, dict_key in key_indexes[:-1]: + if dict_key: + if isinstance(data, dict) and data.has_key(dict_key): + data = data[dict_key] + continue + + data[dict_key] = {} + data = data[dict_key] - if not isinstance(data, dict): - raise YeditException('Invalid add_entry called on a [%s] of type [%s].' % (data, type(data))) + elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1: + data = data[int(arr_ind)] else: - Yedit.add_entry(data[key], rest, item) + return None - else: - data[keys] = item + # process last index for add + # expected list entry + if key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: + data[int(key_indexes[-1][0])] = item + # expected dict entry + elif key_indexes[-1][1] and isinstance(data, dict): + data[key_indexes[-1][1]] = item + + return curr_data @staticmethod - def get_entry(data, keys): + def get_entry(data, key): ''' Get an item from a dictionary with key notation a.b.c d = {'a': {'b': 'c'}}} - keys = a.b + key = a.b return c ''' - if keys and "." in keys: - key, rest = keys.split(".", 1) - if not isinstance(data[key], dict): - raise YeditException('Invalid get_entry called on a [%s] of type [%s].' % (data, type(data))) + if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))): + return None + key_indexes = re.findall(Yedit.re_key, key) + for arr_ind, dict_key in key_indexes: + if dict_key and isinstance(data, dict): + data = data.get(dict_key, None) + elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1: + data = data[int(arr_ind)] else: - return Yedit.get_entry(data[key], rest) - - else: - return data.get(keys, None) + return None + return data def write(self): ''' write to file ''' @@ -355,7 +420,7 @@ class Yedit(object): return False - def get(self): + def load(self, content_type='yaml'): ''' return yaml file ''' contents = self.read() @@ -364,15 +429,27 @@ class Yedit(object): # check if it is yaml try: - self.yaml_dict = yaml.load(contents) + if content_type == 'yaml': + self.yaml_dict = yaml.load(contents) + elif content_type == 'json': + self.yaml_dict = json.loads(contents) except yaml.YAMLError as _: - # Error loading yaml + # Error loading yaml or json return None return self.yaml_dict + def get(self, key): + ''' get a specified key''' + try: + entry = Yedit.get_entry(self.yaml_dict, key) + except KeyError as _: + entry = None + + return entry + def delete(self, key): - ''' put key, value into a yaml file ''' + ''' remove key from a dict''' try: entry = Yedit.get_entry(self.yaml_dict, key) except KeyError as _: @@ -380,12 +457,14 @@ class Yedit(object): if not entry: return (False, self.yaml_dict) - Yedit.remove_entry(self.yaml_dict, key) - self.write() - return (True, self.get()) + result = Yedit.remove_entry(self.yaml_dict, key) + if not result: + return (False, self.yaml_dict) + + return (True, self.yaml_dict) def put(self, key, value): - ''' put key, value into a yaml file ''' + ''' put key, value into a dict ''' try: entry = Yedit.get_entry(self.yaml_dict, key) except KeyError as _: @@ -394,18 +473,19 @@ class Yedit(object): if entry == value: return (False, self.yaml_dict) - Yedit.add_entry(self.yaml_dict, key, value) - self.write() - return (True, self.get()) + result = Yedit.add_entry(self.yaml_dict, key, value) + if not result: + return (False, self.yaml_dict) + + return (True, self.yaml_dict) def create(self, key, value): - ''' create the file ''' + ''' create a yaml file ''' if not self.exists(): self.yaml_dict = {key: value} - self.write() - return (True, self.get()) + return (True, self.yaml_dict) - return (False, self.get()) + return (False, self.yaml_dict) class OCObject(OpenShiftCLI): ''' Class to wrap the oc command line tools ''' @@ -503,6 +583,13 @@ def main(): type='str', choices=['dc', 'deploymentconfig', 'svc', 'service', + 'scc', 'securitycontextconstraints', + 'ns', 'namespace', 'project', 'projects', + 'is', 'imagestream', + 'istag', 'imagestreamtag', + 'bc', 'buildconfig', + 'routes', + 'node', 'secret', ]), delete_after=dict(default=False, type='bool'), diff --git a/roles/lib_openshift_api/library/oc_secret.py b/roles/lib_openshift_api/library/oc_secret.py index 8253fd4ad..a03022e35 100644 --- a/roles/lib_openshift_api/library/oc_secret.py +++ b/roles/lib_openshift_api/library/oc_secret.py @@ -15,7 +15,15 @@ import json import os import shutil import subprocess +import re + import yaml +# This is here because of a bug that causes yaml +# to incorrectly handle timezone info on timestamps +def timestamp_constructor(_, node): + '''return timestamps as strings''' + return str(node.value) +yaml.add_constructor(u'tag:yaml.org,2002:timestamp', timestamp_constructor) # pylint: disable=too-few-public-methods class OpenShiftCLI(object): @@ -39,8 +47,14 @@ class OpenShiftCLI(object): fname = '/tmp/%s' % rname yed = Yedit(fname, res['results'][0]) + changes = [] for key, value in content.items(): - yed.put(key, value) + changes.append(yed.put(key, value)) + + if any([not change[0] for change in changes]): + return {'returncode': 0, 'updated': False} + + yed.write() atexit.register(Utils.cleanup, [fname]) @@ -83,7 +97,9 @@ class OpenShiftCLI(object): cmds = ['/usr/bin/oc'] cmds.extend(cmd) + rval = {} results = '' + err = None if self.verbose: print ' '.join(cmds) @@ -92,27 +108,42 @@ class OpenShiftCLI(object): stdout=subprocess.PIPE, stderr=subprocess.PIPE, env={'KUBECONFIG': self.kubeconfig}) + proc.wait() + stdout = proc.stdout.read() + stderr = proc.stderr.read() + + rval = {"returncode": proc.returncode, + "results": results, + } + if proc.returncode == 0: if output: try: - results = json.loads(proc.stdout.read()) + rval['results'] = json.loads(stdout) except ValueError as err: if "No JSON object could be decoded" in err.message: - results = err.message + err = err.message if self.verbose: - print proc.stderr.read() - print results + print stdout + print stderr print - return {"returncode": proc.returncode, "results": results} + if err: + rval.update({"err": err, + "stderr": stderr, + "stdout": stdout, + "cmd": cmds + }) - return {"returncode": proc.returncode, - "stderr": proc.stderr.read(), - "stdout": proc.stdout.read(), - "results": {} - } + else: + rval.update({"stderr": stderr, + "stdout": stdout, + "results": {}, + }) + + return rval class Utils(object): ''' utilities for openshiftcli modules ''' @@ -186,7 +217,7 @@ class Utils(object): contents = sfd.read() if sfile_type == 'yaml': - contents = yaml.load(contents) + contents = yaml.safe_load(contents) elif sfile_type == 'json': contents = json.loads(contents) @@ -254,15 +285,16 @@ class YeditException(Exception): class Yedit(object): ''' Class to modify yaml files ''' + re_valid_key = r"(((\[-?\d+\])|([a-zA-Z-./]+)).?)+$" + re_key = r"(?:\[(-?\d+)\])|([a-zA-Z-./]+)" - def __init__(self, filename=None, content=None): + def __init__(self, filename=None, content=None, content_type='yaml'): self.content = content self.filename = filename self.__yaml_dict = content + self.content_type = content_type if self.filename and not self.content: - self.get() - elif self.filename and self.content: - self.write() + self.load(content_type=self.content_type) @property def yaml_dict(self): @@ -275,58 +307,91 @@ class Yedit(object): self.__yaml_dict = value @staticmethod - def remove_entry(data, keys): - ''' remove an item from a dictionary with key notation a.b.c - d = {'a': {'b': 'c'}}} - keys = a.b - item = c - ''' - if "." in keys: - key, rest = keys.split(".", 1) - if key in data.keys(): - Yedit.remove_entry(data[key], rest) - else: - del data[keys] + def remove_entry(data, key): + ''' remove data at location key ''' + if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))): + return None + + key_indexes = re.findall(Yedit.re_key, key) + for arr_ind, dict_key in key_indexes[:-1]: + if dict_key and isinstance(data, dict): + data = data.get(dict_key, None) + elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1: + data = data[int(arr_ind)] + else: + return None + + # process last index for remove + # expected list entry + if key_indexes[-1][0]: + if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: + del data[int(key_indexes[-1][0])] + return True + + # expected dict entry + elif key_indexes[-1][1]: + if isinstance(data, dict): + del data[key_indexes[-1][1]] + return True @staticmethod - def add_entry(data, keys, item): - ''' Add an item to a dictionary with key notation a.b.c + def add_entry(data, key, item=None): + ''' Get an item from a dictionary with key notation a.b.c d = {'a': {'b': 'c'}}} - keys = a.b - item = c + key = a.b + return c ''' - if "." in keys: - key, rest = keys.split(".", 1) - if key not in data: - data[key] = {} + if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))): + return None + + curr_data = data + + key_indexes = re.findall(Yedit.re_key, key) + for arr_ind, dict_key in key_indexes[:-1]: + if dict_key: + if isinstance(data, dict) and data.has_key(dict_key): + data = data[dict_key] + continue - if not isinstance(data, dict): - raise YeditException('Invalid add_entry called on a [%s] of type [%s].' % (data, type(data))) + data[dict_key] = {} + data = data[dict_key] + + elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1: + data = data[int(arr_ind)] else: - Yedit.add_entry(data[key], rest, item) + return None - else: - data[keys] = item + # process last index for add + # expected list entry + if key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: + data[int(key_indexes[-1][0])] = item + # expected dict entry + elif key_indexes[-1][1] and isinstance(data, dict): + data[key_indexes[-1][1]] = item + + return curr_data @staticmethod - def get_entry(data, keys): + def get_entry(data, key): ''' Get an item from a dictionary with key notation a.b.c d = {'a': {'b': 'c'}}} - keys = a.b + key = a.b return c ''' - if keys and "." in keys: - key, rest = keys.split(".", 1) - if not isinstance(data[key], dict): - raise YeditException('Invalid get_entry called on a [%s] of type [%s].' % (data, type(data))) + if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))): + return None + key_indexes = re.findall(Yedit.re_key, key) + for arr_ind, dict_key in key_indexes: + if dict_key and isinstance(data, dict): + data = data.get(dict_key, None) + elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1: + data = data[int(arr_ind)] else: - return Yedit.get_entry(data[key], rest) - - else: - return data.get(keys, None) + return None + return data def write(self): ''' write to file ''' @@ -355,7 +420,7 @@ class Yedit(object): return False - def get(self): + def load(self, content_type='yaml'): ''' return yaml file ''' contents = self.read() @@ -364,15 +429,27 @@ class Yedit(object): # check if it is yaml try: - self.yaml_dict = yaml.load(contents) + if content_type == 'yaml': + self.yaml_dict = yaml.load(contents) + elif content_type == 'json': + self.yaml_dict = json.loads(contents) except yaml.YAMLError as _: - # Error loading yaml + # Error loading yaml or json return None return self.yaml_dict + def get(self, key): + ''' get a specified key''' + try: + entry = Yedit.get_entry(self.yaml_dict, key) + except KeyError as _: + entry = None + + return entry + def delete(self, key): - ''' put key, value into a yaml file ''' + ''' remove key from a dict''' try: entry = Yedit.get_entry(self.yaml_dict, key) except KeyError as _: @@ -380,12 +457,14 @@ class Yedit(object): if not entry: return (False, self.yaml_dict) - Yedit.remove_entry(self.yaml_dict, key) - self.write() - return (True, self.get()) + result = Yedit.remove_entry(self.yaml_dict, key) + if not result: + return (False, self.yaml_dict) + + return (True, self.yaml_dict) def put(self, key, value): - ''' put key, value into a yaml file ''' + ''' put key, value into a dict ''' try: entry = Yedit.get_entry(self.yaml_dict, key) except KeyError as _: @@ -394,18 +473,19 @@ class Yedit(object): if entry == value: return (False, self.yaml_dict) - Yedit.add_entry(self.yaml_dict, key, value) - self.write() - return (True, self.get()) + result = Yedit.add_entry(self.yaml_dict, key, value) + if not result: + return (False, self.yaml_dict) + + return (True, self.yaml_dict) def create(self, key, value): - ''' create the file ''' + ''' create a yaml file ''' if not self.exists(): self.yaml_dict = {key: value} - self.write() - return (True, self.get()) + return (True, self.yaml_dict) - return (False, self.get()) + return (False, self.yaml_dict) class Secret(OpenShiftCLI): ''' Class to wrap the oc command line tools diff --git a/roles/lib_yaml_editor/build/ansible/yedit.py b/roles/lib_yaml_editor/build/ansible/yedit.py index bf868fb71..dab3d6347 100644 --- a/roles/lib_yaml_editor/build/ansible/yedit.py +++ b/roles/lib_yaml_editor/build/ansible/yedit.py @@ -24,7 +24,7 @@ def main(): yamlfile = Yedit(module.params['src'], module.params['content']) - rval = yamlfile.get() + rval = yamlfile.load() if not rval and state != 'present': module.fail_json(msg='Error opening file [%s]. Verify that the' + \ ' file exists, that it is has correct permissions, and is valid yaml.') @@ -45,13 +45,16 @@ def main(): if rval: rval = yamlfile.put(module.params['key'], value) + if rval[0]: + yamlfile.write() module.exit_json(changed=rval[0], results=rval[1], state="present") if not module.params['content']: rval = yamlfile.create(module.params['key'], value) else: - yamlfile.write() - rval = yamlfile.get() + rval = yamlfile.load() + yamlfile.write() + module.exit_json(changed=rval[0], results=rval[1], state="present") module.exit_json(failed=True, diff --git a/roles/lib_yaml_editor/build/generate.py b/roles/lib_yaml_editor/build/generate.py index 0df4efb92..312e4d0ee 100755 --- a/roles/lib_yaml_editor/build/generate.py +++ b/roles/lib_yaml_editor/build/generate.py @@ -15,19 +15,20 @@ GEN_STR = "#!/usr/bin/env python\n" + \ "# | |) | (_) | | .` | (_) || | | _|| |) | | | |\n" + \ "# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|\n" +OPENSHIFT_ANSIBLE_PATH = os.path.dirname(os.path.realpath(__file__)) + FILES = {'yedit.py': ['src/base.py', 'src/yedit.py', 'ansible/yedit.py'], } - def main(): ''' combine the necessary files to create the ansible module ''' - openshift_ansible = ('../library/') + library = os.path.join(OPENSHIFT_ANSIBLE_PATH, '..', 'library/') for fname, parts in FILES.items(): - with open(os.path.join(openshift_ansible, fname), 'w') as afd: + with open(os.path.join(library, fname), 'w') as afd: afd.seek(0) afd.write(GEN_STR) for fpart in parts: - with open(fpart) as pfd: + with open(os.path.join(OPENSHIFT_ANSIBLE_PATH, fpart)) as pfd: # first line is pylint disable so skip it for idx, line in enumerate(pfd): if idx == 0 and 'skip-file' in line: diff --git a/roles/lib_yaml_editor/build/src/base.py b/roles/lib_yaml_editor/build/src/base.py index ad8b041cf..9e43d45dc 100644 --- a/roles/lib_yaml_editor/build/src/base.py +++ b/roles/lib_yaml_editor/build/src/base.py @@ -5,5 +5,13 @@ module for managing yaml files ''' import os +import re + import yaml +# This is here because of a bug that causes yaml +# to incorrectly handle timezone info on timestamps +def timestamp_constructor(_, node): + ''' return timestamps as strings''' + return str(node.value) +yaml.add_constructor(u'tag:yaml.org,2002:timestamp', timestamp_constructor) diff --git a/roles/lib_yaml_editor/build/src/yedit.py b/roles/lib_yaml_editor/build/src/yedit.py index 4f6a91d8b..642733914 100644 --- a/roles/lib_yaml_editor/build/src/yedit.py +++ b/roles/lib_yaml_editor/build/src/yedit.py @@ -6,15 +6,16 @@ class YeditException(Exception): class Yedit(object): ''' Class to modify yaml files ''' + re_valid_key = r"(((\[-?\d+\])|([a-zA-Z-./]+)).?)+$" + re_key = r"(?:\[(-?\d+)\])|([a-zA-Z-./]+)" - def __init__(self, filename=None, content=None): + def __init__(self, filename=None, content=None, content_type='yaml'): self.content = content self.filename = filename self.__yaml_dict = content + self.content_type = content_type if self.filename and not self.content: - self.get() - elif self.filename and self.content: - self.write() + self.load(content_type=self.content_type) @property def yaml_dict(self): @@ -27,58 +28,91 @@ class Yedit(object): self.__yaml_dict = value @staticmethod - def remove_entry(data, keys): - ''' remove an item from a dictionary with key notation a.b.c - d = {'a': {'b': 'c'}}} - keys = a.b - item = c - ''' - if "." in keys: - key, rest = keys.split(".", 1) - if key in data.keys(): - Yedit.remove_entry(data[key], rest) - else: - del data[keys] + def remove_entry(data, key): + ''' remove data at location key ''' + if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))): + return None + + key_indexes = re.findall(Yedit.re_key, key) + for arr_ind, dict_key in key_indexes[:-1]: + if dict_key and isinstance(data, dict): + data = data.get(dict_key, None) + elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1: + data = data[int(arr_ind)] + else: + return None + + # process last index for remove + # expected list entry + if key_indexes[-1][0]: + if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: + del data[int(key_indexes[-1][0])] + return True + + # expected dict entry + elif key_indexes[-1][1]: + if isinstance(data, dict): + del data[key_indexes[-1][1]] + return True @staticmethod - def add_entry(data, keys, item): - ''' Add an item to a dictionary with key notation a.b.c + def add_entry(data, key, item=None): + ''' Get an item from a dictionary with key notation a.b.c d = {'a': {'b': 'c'}}} - keys = a.b - item = c + key = a.b + return c ''' - if "." in keys: - key, rest = keys.split(".", 1) - if key not in data: - data[key] = {} + if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))): + return None + + curr_data = data + + key_indexes = re.findall(Yedit.re_key, key) + for arr_ind, dict_key in key_indexes[:-1]: + if dict_key: + if isinstance(data, dict) and data.has_key(dict_key): + data = data[dict_key] + continue + + data[dict_key] = {} + data = data[dict_key] - if not isinstance(data, dict): - raise YeditException('Invalid add_entry called on a [%s] of type [%s].' % (data, type(data))) + elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1: + data = data[int(arr_ind)] else: - Yedit.add_entry(data[key], rest, item) + return None - else: - data[keys] = item + # process last index for add + # expected list entry + if key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: + data[int(key_indexes[-1][0])] = item + # expected dict entry + elif key_indexes[-1][1] and isinstance(data, dict): + data[key_indexes[-1][1]] = item + + return curr_data @staticmethod - def get_entry(data, keys): + def get_entry(data, key): ''' Get an item from a dictionary with key notation a.b.c d = {'a': {'b': 'c'}}} - keys = a.b + key = a.b return c ''' - if keys and "." in keys: - key, rest = keys.split(".", 1) - if not isinstance(data[key], dict): - raise YeditException('Invalid get_entry called on a [%s] of type [%s].' % (data, type(data))) + if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))): + return None + key_indexes = re.findall(Yedit.re_key, key) + for arr_ind, dict_key in key_indexes: + if dict_key and isinstance(data, dict): + data = data.get(dict_key, None) + elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1: + data = data[int(arr_ind)] else: - return Yedit.get_entry(data[key], rest) - - else: - return data.get(keys, None) + return None + return data def write(self): ''' write to file ''' @@ -107,7 +141,7 @@ class Yedit(object): return False - def get(self): + def load(self, content_type='yaml'): ''' return yaml file ''' contents = self.read() @@ -116,15 +150,27 @@ class Yedit(object): # check if it is yaml try: - self.yaml_dict = yaml.load(contents) + if content_type == 'yaml': + self.yaml_dict = yaml.load(contents) + elif content_type == 'json': + self.yaml_dict = json.loads(contents) except yaml.YAMLError as _: - # Error loading yaml + # Error loading yaml or json return None return self.yaml_dict + def get(self, key): + ''' get a specified key''' + try: + entry = Yedit.get_entry(self.yaml_dict, key) + except KeyError as _: + entry = None + + return entry + def delete(self, key): - ''' put key, value into a yaml file ''' + ''' remove key from a dict''' try: entry = Yedit.get_entry(self.yaml_dict, key) except KeyError as _: @@ -132,12 +178,14 @@ class Yedit(object): if not entry: return (False, self.yaml_dict) - Yedit.remove_entry(self.yaml_dict, key) - self.write() - return (True, self.get()) + result = Yedit.remove_entry(self.yaml_dict, key) + if not result: + return (False, self.yaml_dict) + + return (True, self.yaml_dict) def put(self, key, value): - ''' put key, value into a yaml file ''' + ''' put key, value into a dict ''' try: entry = Yedit.get_entry(self.yaml_dict, key) except KeyError as _: @@ -146,15 +194,16 @@ class Yedit(object): if entry == value: return (False, self.yaml_dict) - Yedit.add_entry(self.yaml_dict, key, value) - self.write() - return (True, self.get()) + result = Yedit.add_entry(self.yaml_dict, key, value) + if not result: + return (False, self.yaml_dict) + + return (True, self.yaml_dict) def create(self, key, value): - ''' create the file ''' + ''' create a yaml file ''' if not self.exists(): self.yaml_dict = {key: value} - self.write() - return (True, self.get()) + return (True, self.yaml_dict) - return (False, self.get()) + return (False, self.yaml_dict) diff --git a/roles/lib_yaml_editor/build/test/foo.yml b/roles/lib_yaml_editor/build/test/foo.yml index 2a7a89ce2..20e9ff3fe 100644 --- a/roles/lib_yaml_editor/build/test/foo.yml +++ b/roles/lib_yaml_editor/build/test/foo.yml @@ -1 +1 @@ -foo: barplus +foo: bar diff --git a/roles/lib_yaml_editor/library/yedit.py b/roles/lib_yaml_editor/library/yedit.py index f375fd8e2..b7ae45b31 100644 --- a/roles/lib_yaml_editor/library/yedit.py +++ b/roles/lib_yaml_editor/library/yedit.py @@ -12,7 +12,15 @@ module for managing yaml files ''' import os +import re + import yaml +# This is here because of a bug that causes yaml +# to incorrectly handle timezone info on timestamps +def timestamp_constructor(_, node): + ''' return timestamps as strings''' + return str(node.value) +yaml.add_constructor(u'tag:yaml.org,2002:timestamp', timestamp_constructor) class YeditException(Exception): @@ -21,15 +29,16 @@ class YeditException(Exception): class Yedit(object): ''' Class to modify yaml files ''' + re_valid_key = r"(((\[-?\d+\])|([a-zA-Z-./]+)).?)+$" + re_key = r"(?:\[(-?\d+)\])|([a-zA-Z-./]+)" - def __init__(self, filename=None, content=None): + def __init__(self, filename=None, content=None, content_type='yaml'): self.content = content self.filename = filename self.__yaml_dict = content + self.content_type = content_type if self.filename and not self.content: - self.get() - elif self.filename and self.content: - self.write() + self.load(content_type=self.content_type) @property def yaml_dict(self): @@ -42,58 +51,91 @@ class Yedit(object): self.__yaml_dict = value @staticmethod - def remove_entry(data, keys): - ''' remove an item from a dictionary with key notation a.b.c - d = {'a': {'b': 'c'}}} - keys = a.b - item = c - ''' - if "." in keys: - key, rest = keys.split(".", 1) - if key in data.keys(): - Yedit.remove_entry(data[key], rest) - else: - del data[keys] + def remove_entry(data, key): + ''' remove data at location key ''' + if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))): + return None + + key_indexes = re.findall(Yedit.re_key, key) + for arr_ind, dict_key in key_indexes[:-1]: + if dict_key and isinstance(data, dict): + data = data.get(dict_key, None) + elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1: + data = data[int(arr_ind)] + else: + return None + + # process last index for remove + # expected list entry + if key_indexes[-1][0]: + if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: + del data[int(key_indexes[-1][0])] + return True + + # expected dict entry + elif key_indexes[-1][1]: + if isinstance(data, dict): + del data[key_indexes[-1][1]] + return True @staticmethod - def add_entry(data, keys, item): - ''' Add an item to a dictionary with key notation a.b.c + def add_entry(data, key, item=None): + ''' Get an item from a dictionary with key notation a.b.c d = {'a': {'b': 'c'}}} - keys = a.b - item = c + key = a.b + return c ''' - if "." in keys: - key, rest = keys.split(".", 1) - if key not in data: - data[key] = {} + if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))): + return None - if not isinstance(data, dict): - raise YeditException('Invalid add_entry called on a [%s] of type [%s].' % (data, type(data))) + curr_data = data + + key_indexes = re.findall(Yedit.re_key, key) + for arr_ind, dict_key in key_indexes[:-1]: + if dict_key: + if isinstance(data, dict) and data.has_key(dict_key): + data = data[dict_key] + continue + + data[dict_key] = {} + data = data[dict_key] + + elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1: + data = data[int(arr_ind)] else: - Yedit.add_entry(data[key], rest, item) + return None - else: - data[keys] = item + # process last index for add + # expected list entry + if key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: + data[int(key_indexes[-1][0])] = item + # expected dict entry + elif key_indexes[-1][1] and isinstance(data, dict): + data[key_indexes[-1][1]] = item + + return curr_data @staticmethod - def get_entry(data, keys): + def get_entry(data, key): ''' Get an item from a dictionary with key notation a.b.c d = {'a': {'b': 'c'}}} - keys = a.b + key = a.b return c ''' - if keys and "." in keys: - key, rest = keys.split(".", 1) - if not isinstance(data[key], dict): - raise YeditException('Invalid get_entry called on a [%s] of type [%s].' % (data, type(data))) + if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))): + return None + key_indexes = re.findall(Yedit.re_key, key) + for arr_ind, dict_key in key_indexes: + if dict_key and isinstance(data, dict): + data = data.get(dict_key, None) + elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1: + data = data[int(arr_ind)] else: - return Yedit.get_entry(data[key], rest) - - else: - return data.get(keys, None) + return None + return data def write(self): ''' write to file ''' @@ -122,7 +164,7 @@ class Yedit(object): return False - def get(self): + def load(self, content_type='yaml'): ''' return yaml file ''' contents = self.read() @@ -131,15 +173,27 @@ class Yedit(object): # check if it is yaml try: - self.yaml_dict = yaml.load(contents) + if content_type == 'yaml': + self.yaml_dict = yaml.load(contents) + elif content_type == 'json': + self.yaml_dict = json.loads(contents) except yaml.YAMLError as _: - # Error loading yaml + # Error loading yaml or json return None return self.yaml_dict + def get(self, key): + ''' get a specified key''' + try: + entry = Yedit.get_entry(self.yaml_dict, key) + except KeyError as _: + entry = None + + return entry + def delete(self, key): - ''' put key, value into a yaml file ''' + ''' remove key from a dict''' try: entry = Yedit.get_entry(self.yaml_dict, key) except KeyError as _: @@ -147,12 +201,14 @@ class Yedit(object): if not entry: return (False, self.yaml_dict) - Yedit.remove_entry(self.yaml_dict, key) - self.write() - return (True, self.get()) + result = Yedit.remove_entry(self.yaml_dict, key) + if not result: + return (False, self.yaml_dict) + + return (True, self.yaml_dict) def put(self, key, value): - ''' put key, value into a yaml file ''' + ''' put key, value into a dict ''' try: entry = Yedit.get_entry(self.yaml_dict, key) except KeyError as _: @@ -161,18 +217,19 @@ class Yedit(object): if entry == value: return (False, self.yaml_dict) - Yedit.add_entry(self.yaml_dict, key, value) - self.write() - return (True, self.get()) + result = Yedit.add_entry(self.yaml_dict, key, value) + if not result: + return (False, self.yaml_dict) + + return (True, self.yaml_dict) def create(self, key, value): - ''' create the file ''' + ''' create a yaml file ''' if not self.exists(): self.yaml_dict = {key: value} - self.write() - return (True, self.get()) + return (True, self.yaml_dict) - return (False, self.get()) + return (False, self.yaml_dict) def main(): ''' @@ -198,7 +255,7 @@ def main(): yamlfile = Yedit(module.params['src'], module.params['content']) - rval = yamlfile.get() + rval = yamlfile.load() if not rval and state != 'present': module.fail_json(msg='Error opening file [%s]. Verify that the' + \ ' file exists, that it is has correct permissions, and is valid yaml.') @@ -219,13 +276,16 @@ def main(): if rval: rval = yamlfile.put(module.params['key'], value) + if rval[0]: + yamlfile.write() module.exit_json(changed=rval[0], results=rval[1], state="present") if not module.params['content']: rval = yamlfile.create(module.params['key'], value) else: - yamlfile.write() - rval = yamlfile.get() + rval = yamlfile.load() + yamlfile.write() + module.exit_json(changed=rval[0], results=rval[1], state="present") module.exit_json(failed=True, diff --git a/roles/openshift_cli/meta/main.yml b/roles/openshift_cli/meta/main.yml index 2c982e278..223cb768d 100644 --- a/roles/openshift_cli/meta/main.yml +++ b/roles/openshift_cli/meta/main.yml @@ -13,6 +13,5 @@ galaxy_info: - cloud dependencies: - role: openshift_docker - when: openshift.common.is_containerized | bool - role: openshift_common - role: openshift_cli_facts diff --git a/roles/openshift_docker/meta/main.yml b/roles/openshift_docker/meta/main.yml index 60efd4e45..d98f953ea 100644 --- a/roles/openshift_docker/meta/main.yml +++ b/roles/openshift_docker/meta/main.yml @@ -12,5 +12,6 @@ galaxy_info: categories: - cloud dependencies: +- role: openshift_repos - role: openshift_docker_facts - role: docker diff --git a/roles/openshift_docker_facts/tasks/main.yml b/roles/openshift_docker_facts/tasks/main.yml index 26b46aa94..1848619e0 100644 --- a/roles/openshift_docker_facts/tasks/main.yml +++ b/roles/openshift_docker_facts/tasks/main.yml @@ -37,3 +37,19 @@ - set_fact: docker_options: "{{ openshift.docker.options | default(omit) }}" when: not openshift.docker.hosted_registry_insecure | default(False) | bool + +# Avoid docker 1.9 when installing origin < 1.2 or OSE < 3.2 on RHEL/Centos and +# See: https://bugzilla.redhat.com/show_bug.cgi?id=1304038 +- name: Gather common package version + command: > + {{ repoquery_cmd }} --qf '%{version}' "{{ openshift.common.service_type}}" + register: common_version + failed_when: false + changed_when: false + when: not openshift.common.is_atomic | bool + +- name: Set docker version to be installed + set_fact: + docker_version: "{{ '1.8.2' }}" + when: " ( common_version.stdout | default('0.0', True) | version_compare('3.2','<') and openshift.common.service_type == 'atomic-openshift' ) or + ( common_version.stdout | default('0.0', True) | version_compare('1.2','<') and openshift.common.service_type == 'origin' )" diff --git a/roles/openshift_docker_facts/vars/main.yml b/roles/openshift_docker_facts/vars/main.yml new file mode 100644 index 000000000..f7ad1b329 --- /dev/null +++ b/roles/openshift_docker_facts/vars/main.yml @@ -0,0 +1,2 @@ +--- +repoquery_cmd: "{{ 'dnf repoquery --latest-limit 1 -d 0' if ansible_pkg_mgr == 'dnf' else 'repoquery' }}" diff --git a/roles/openshift_facts/tasks/main.yml b/roles/openshift_facts/tasks/main.yml index a2eb27fbb..36def57c8 100644 --- a/roles/openshift_facts/tasks/main.yml +++ b/roles/openshift_facts/tasks/main.yml @@ -15,7 +15,7 @@ - set_fact: l_is_atomic: "{{ ostree_output.rc == 0 }}" - set_fact: - l_is_containerized: "{{ l_is_atomic or containerized | default(false) | bool }}" + l_is_containerized: "{{ (l_is_atomic | bool) or (containerized | default(false) | bool) }}" - name: Ensure PyYaml is installed action: "{{ ansible_pkg_mgr }} name=PyYAML state=present" diff --git a/roles/openshift_master/meta/main.yml b/roles/openshift_master/meta/main.yml index 8ae6bc371..7ab913eea 100644 --- a/roles/openshift_master/meta/main.yml +++ b/roles/openshift_master/meta/main.yml @@ -12,6 +12,7 @@ galaxy_info: categories: - cloud dependencies: +- role: openshift_docker - role: openshift_cli - role: openshift_cloud_provider - role: openshift_master_facts diff --git a/roles/openshift_master_cluster/tasks/main.yml b/roles/openshift_master_cluster/tasks/main.yml index 40705d357..0543872c9 100644 --- a/roles/openshift_master_cluster/tasks/main.yml +++ b/roles/openshift_master_cluster/tasks/main.yml @@ -3,6 +3,10 @@ msg: "Not possible on atomic hosts for now" when: openshift.common.is_containerized | bool +- fail: + msg: "Pacemaker HA is unsupported on OpenShift Enterprise 3.2 and Origin 1.2" + when: openshift.master.cluster_method == "pacemaker" and openshift.common.version_gte_3_2_or_1_2 | bool + - name: Test if cluster is already configured command: pcs status register: pcs_status diff --git a/roles/os_zabbix/vars/template_heartbeat.yml b/roles/os_zabbix/vars/template_heartbeat.yml index 8dbe0d0d6..ec953c79b 100644 --- a/roles/os_zabbix/vars/template_heartbeat.yml +++ b/roles/os_zabbix/vars/template_heartbeat.yml @@ -11,3 +11,8 @@ g_template_heartbeat: expression: '{Template Heartbeat:heartbeat.ping.nodata(20m)}=1' priority: avg url: 'https://github.com/openshift/ops-sop/blob/master/V3/Alerts/check_node_heartbeat.asciidoc' + + - name: 'Heartbeat.ping has failed (60 min) on {HOST.NAME}' + expression: '{Template Heartbeat:heartbeat.ping.nodata(60m)}=1' + priority: high + url: 'https://github.com/openshift/ops-sop/blob/master/V3/Alerts/check_node_heartbeat.asciidoc' diff --git a/test/env-setup b/test/env-setup index b05df0f9e..7456a641b 100644 --- a/test/env-setup +++ b/test/env-setup @@ -2,7 +2,7 @@ CUR_PATH=$(pwd) -PREFIX_PYTHONPATH=$CUR_PATH/inventory/:$CUR_PATH/roles/lib_yaml_editor/build/src +PREFIX_PYTHONPATH=$CUR_PATH/inventory/:$CUR_PATH/roles/lib_yaml_editor/library export PYTHONPATH=$PREFIX_PYTHONPATH:$PYTHONPATH diff --git a/test/units/yedit_test.py b/test/units/yedit_test.py index e701cfa7c..09a65e888 100755 --- a/test/units/yedit_test.py +++ b/test/units/yedit_test.py @@ -16,7 +16,7 @@ class YeditTest(unittest.TestCase): Test class for yedit ''' data = {'a': 'a', - 'b': {'c': {'d': ['e', 'f', 'g']}}, + 'b': {'c': {'d': [{'e': 'x'}, 'f', 'g']}}, } filename = 'yedit_test.yml' @@ -27,10 +27,9 @@ class YeditTest(unittest.TestCase): yed.yaml_dict = YeditTest.data yed.write() - def test_get(self): + def test_load(self): ''' Testing a get ''' yed = Yedit('yedit_test.yml') - self.assertEqual(yed.yaml_dict, self.data) def test_write(self): @@ -38,7 +37,6 @@ class YeditTest(unittest.TestCase): yed = Yedit('yedit_test.yml') yed.put('key1', 1) yed.write() - yed.get() self.assertTrue(yed.yaml_dict.has_key('key1')) self.assertEqual(yed.yaml_dict['key1'], 1) @@ -47,14 +45,15 @@ class YeditTest(unittest.TestCase): yed = Yedit('yedit_test.yml') yed.put('x.y.z', 'modified') yed.write() - self.assertEqual(Yedit.get_entry(yed.get(), 'x.y.z'), 'modified') + yed.load() + self.assertEqual(yed.get('x.y.z'), 'modified') def test_delete_a(self): '''Testing a simple delete ''' yed = Yedit('yedit_test.yml') yed.delete('a') yed.write() - yed.get() + yed.load() self.assertTrue(not yed.yaml_dict.has_key('a')) def test_delete_b_c(self): @@ -62,7 +61,7 @@ class YeditTest(unittest.TestCase): yed = Yedit('yedit_test.yml') yed.delete('b.c') yed.write() - yed.get() + yed.load() self.assertTrue(yed.yaml_dict.has_key('b')) self.assertFalse(yed.yaml_dict['b'].has_key('c')) @@ -72,7 +71,7 @@ class YeditTest(unittest.TestCase): yed = Yedit('yedit_test.yml') yed.create('foo', 'bar') yed.write() - yed.get() + yed.load() self.assertTrue(yed.yaml_dict.has_key('foo')) self.assertTrue(yed.yaml_dict['foo'] == 'bar') @@ -81,10 +80,61 @@ class YeditTest(unittest.TestCase): content = {"foo": "bar"} yed = Yedit("yedit_test.yml", content) yed.write() - yed.get() + yed.load() self.assertTrue(yed.yaml_dict.has_key('foo')) self.assertTrue(yed.yaml_dict['foo'], 'bar') + def test_array_insert(self): + '''Testing a create with content ''' + yed = Yedit("yedit_test.yml") + yed.put('b.c.d[0]', 'inject') + self.assertTrue(yed.get('b.c.d[0]') == 'inject') + + def test_array_insert_first_index(self): + '''Testing a create with content ''' + yed = Yedit("yedit_test.yml") + yed.put('b.c.d[0]', 'inject') + self.assertTrue(yed.get('b.c.d[1]') == 'f') + + def test_array_insert_second_index(self): + '''Testing a create with content ''' + yed = Yedit("yedit_test.yml") + yed.put('b.c.d[0]', 'inject') + self.assertTrue(yed.get('b.c.d[2]') == 'g') + + def test_dict_array_dict_access(self): + '''Testing a create with content''' + yed = Yedit("yedit_test.yml") + yed.put('b.c.d[0]', [{'x': {'y': 'inject'}}]) + self.assertTrue(yed.get('b.c.d[0].[0].x.y') == 'inject') + + def test_dict_array_dict_replace(self): + '''Testing multilevel delete''' + yed = Yedit("yedit_test.yml") + yed.put('b.c.d[0]', [{'x': {'y': 'inject'}}]) + yed.put('b.c.d[0].[0].x.y', 'testing') + self.assertTrue(yed.yaml_dict.has_key('b')) + self.assertTrue(yed.yaml_dict['b'].has_key('c')) + self.assertTrue(yed.yaml_dict['b']['c'].has_key('d')) + self.assertTrue(isinstance(yed.yaml_dict['b']['c']['d'], list)) + self.assertTrue(isinstance(yed.yaml_dict['b']['c']['d'][0], list)) + self.assertTrue(isinstance(yed.yaml_dict['b']['c']['d'][0][0], dict)) + self.assertTrue(yed.yaml_dict['b']['c']['d'][0][0]['x'].has_key('y')) + self.assertTrue(yed.yaml_dict['b']['c']['d'][0][0]['x']['y'], 'testing') + + def test_dict_array_dict_remove(self): + '''Testing multilevel delete''' + yed = Yedit("yedit_test.yml") + yed.put('b.c.d[0]', [{'x': {'y': 'inject'}}]) + yed.delete('b.c.d[0].[0].x.y') + self.assertTrue(yed.yaml_dict.has_key('b')) + self.assertTrue(yed.yaml_dict['b'].has_key('c')) + self.assertTrue(yed.yaml_dict['b']['c'].has_key('d')) + self.assertTrue(isinstance(yed.yaml_dict['b']['c']['d'], list)) + self.assertTrue(isinstance(yed.yaml_dict['b']['c']['d'][0], list)) + self.assertTrue(isinstance(yed.yaml_dict['b']['c']['d'][0][0], dict)) + self.assertFalse(yed.yaml_dict['b']['c']['d'][0][0]['x'].has_key('y')) + def tearDown(self): '''TearDown method''' os.unlink(YeditTest.filename) diff --git a/utils/src/ooinstall/cli_installer.py b/utils/src/ooinstall/cli_installer.py index c53ca7b18..dd9d517f1 100644 --- a/utils/src/ooinstall/cli_installer.py +++ b/utils/src/ooinstall/cli_installer.py @@ -666,7 +666,7 @@ def get_hosts_to_run_on(oo_cfg, callback_facts, unattended, force, verbose): openshift_ansible.set_config(oo_cfg) click.echo('Gathering information from hosts...') callback_facts, error = openshift_ansible.default_facts(oo_cfg.hosts, verbose) - if error: + if error or callback_facts is None: click.echo("There was a problem fetching the required information. See " \ "{} for details.".format(oo_cfg.settings['ansible_log_path'])) sys.exit(1) @@ -780,42 +780,67 @@ def uninstall(ctx): @click.command() +@click.option('--latest-minor', '-l', is_flag=True, default=False) +@click.option('--next-major', '-n', is_flag=True, default=False) @click.pass_context -def upgrade(ctx): +def upgrade(ctx, latest_minor, next_major): oo_cfg = ctx.obj['oo_cfg'] verbose = ctx.obj['verbose'] + upgrade_mappings = { + '3.0':{ + 'minor_version' :'3.0', + 'minor_playbook':'v3_0_minor/upgrade.yml', + 'major_version' :'3.1', + 'major_playbook':'v3_0_to_v3_1/upgrade.yml', + }, + '3.1':{ + 'minor_version' :'3.1', + 'minor_playbook':'v3_1_minor/upgrade.yml', + 'major_playbook':'v3_1_to_v3_2/upgrade.yml', + 'major_version' :'3.2', + } + } + if len(oo_cfg.hosts) == 0: click.echo("No hosts defined in: %s" % oo_cfg.config_path) sys.exit(1) old_variant = oo_cfg.settings['variant'] old_version = oo_cfg.settings['variant_version'] - + mapping = upgrade_mappings.get(old_version) message = """ This tool will help you upgrade your existing OpenShift installation. """ click.echo(message) - click.echo("Version {} found. Do you want to update to the latest version of {} " \ - "or migrate to the next major release?".format(old_version, old_version)) - resp = click.prompt("(1) Update to latest {} (2) Migrate to next relese".format(old_version)) - if resp == "2": - # TODO: Make this a lot more flexible - new_version = "3.1" + if not (latest_minor or next_major): + click.echo("Version {} found. Do you want to update to the latest version of {} " \ + "or migrate to the next major release?".format(old_version, old_version)) + response = click.prompt("(1) Update to latest {} " \ + "(2) Migrate to next release".format(old_version), + type=click.Choice(['1', '2']),) + if response == "1": + latest_minor = True + if response == "2": + next_major = True + + if next_major: + playbook = mapping['major_playbook'] + new_version = mapping['major_version'] # Update config to reflect the version we're targetting, we'll write # to disk once ansible completes successfully, not before. + oo_cfg.settings['variant_version'] = new_version if oo_cfg.settings['variant'] == 'enterprise': oo_cfg.settings['variant'] = 'openshift-enterprise' - version = find_variant(oo_cfg.settings['variant'])[1] - oo_cfg.settings['variant_version'] = version.name - else: - new_version = old_version + + if latest_minor: + playbook = mapping['minor_playbook'] + new_version = mapping['minor_version'] click.echo("Openshift will be upgraded from %s %s to %s %s on the following hosts:\n" % ( - old_variant, old_version, oo_cfg.settings['variant'], - oo_cfg.settings['variant_version'])) + old_variant, old_version, oo_cfg.settings['variant'], new_version)) for host in oo_cfg.hosts: click.echo(" * %s" % host.connect_to) @@ -826,7 +851,7 @@ def upgrade(ctx): click.echo("Upgrade cancelled.") sys.exit(0) - retcode = openshift_ansible.run_upgrade_playbook(old_version, new_version, verbose) + retcode = openshift_ansible.run_upgrade_playbook(playbook, verbose) if retcode > 0: click.echo("Errors encountered during upgrade, please check %s." % oo_cfg.settings['ansible_log_path']) @@ -837,8 +862,10 @@ def upgrade(ctx): @click.command() @click.option('--force', '-f', is_flag=True, default=False) +@click.option('--gen-inventory', is_flag=True, default=False, + help="Generate an ansible inventory file and exit.") @click.pass_context -def install(ctx, force): +def install(ctx, force, gen_inventory): oo_cfg = ctx.obj['oo_cfg'] verbose = ctx.obj['verbose'] @@ -853,7 +880,7 @@ def install(ctx, force): click.echo('Gathering information from hosts...') callback_facts, error = openshift_ansible.default_facts(oo_cfg.hosts, verbose) - if error: + if error or callback_facts is None: click.echo("There was a problem fetching the required information. " \ "Please see {} for details.".format(oo_cfg.settings['ansible_log_path'])) sys.exit(1) @@ -861,7 +888,6 @@ def install(ctx, force): hosts_to_run_on, callback_facts = get_hosts_to_run_on( oo_cfg, callback_facts, ctx.obj['unattended'], force, verbose) - click.echo('Writing config to: %s' % oo_cfg.config_path) # We already verified this is not the case for unattended installs, so this can # only trigger for live CLI users: @@ -871,7 +897,18 @@ def install(ctx, force): if len(oo_cfg.calc_missing_facts()) > 0: confirm_hosts_facts(oo_cfg, callback_facts) + # Write quick installer config file to disk: oo_cfg.save_to_disk() + # Write ansible inventory file to disk: + inventory_file = openshift_ansible.generate_inventory(hosts_to_run_on) + + click.echo() + click.echo('Wrote atomic-openshift-installer config: %s' % oo_cfg.config_path) + click.echo("Wrote ansible inventory: %s" % inventory_file) + click.echo() + + if gen_inventory: + sys.exit(0) click.echo('Ready to run installation process.') message = """ @@ -880,8 +917,8 @@ If changes are needed please edit the config file above and re-run. if not ctx.obj['unattended']: confirm_continue(message) - error = openshift_ansible.run_main_playbook(oo_cfg.hosts, - hosts_to_run_on, verbose) + error = openshift_ansible.run_main_playbook(inventory_file, oo_cfg.hosts, + hosts_to_run_on, verbose) if error: # The bootstrap script will print out the log location. message = """ diff --git a/utils/src/ooinstall/oo_config.py b/utils/src/ooinstall/oo_config.py index c9498542f..0f1f5caf7 100644 --- a/utils/src/ooinstall/oo_config.py +++ b/utils/src/ooinstall/oo_config.py @@ -198,7 +198,7 @@ class OOConfig(object): self.settings['ansible_ssh_user'] = '' self.settings['ansible_inventory_path'] = \ - '{}/hosts'.format(self.settings['ansible_inventory_directory']) + '{}/hosts'.format(os.path.dirname(self.config_path)) # clean up any empty sets for setting in self.settings.keys(): diff --git a/utils/src/ooinstall/openshift_ansible.py b/utils/src/ooinstall/openshift_ansible.py index 2b95702bf..28b157e8e 100644 --- a/utils/src/ooinstall/openshift_ansible.py +++ b/utils/src/ooinstall/openshift_ansible.py @@ -213,9 +213,8 @@ def default_facts(hosts, verbose=False): return load_system_facts(inventory_file, os_facts_path, facts_env, verbose) -def run_main_playbook(hosts, hosts_to_run_on, verbose=False): +def run_main_playbook(inventory_file, hosts, hosts_to_run_on, verbose=False): global CFG - inventory_file = generate_inventory(hosts_to_run_on) if len(hosts_to_run_on) != len(hosts): main_playbook_path = os.path.join(CFG.ansible_playbook_directory, 'playbooks/byo/openshift-node/scaleup.yml') @@ -251,18 +250,10 @@ def run_uninstall_playbook(verbose=False): return run_ansible(playbook, inventory_file, facts_env, verbose) -def run_upgrade_playbook(old_version, new_version, verbose=False): - # TODO: do not hardcode the upgrade playbook, add ability to select the - # right playbook depending on the type of upgrade. - old_version = old_version.replace('.', '_') - new_version = old_version.replace('.', '_') - if old_version == new_version: - playbook = os.path.join(CFG.settings['ansible_playbook_directory'], - 'playbooks/byo/openshift-cluster/upgrades/v{}_minor/upgrade.yml'.format(new_version)) - else: - playbook = os.path.join(CFG.settings['ansible_playbook_directory'], - 'playbooks/byo/openshift-cluster/upgrades/v{}_to_v{}/upgrade.yml'.format(old_version, - new_version)) +def run_upgrade_playbook(playbook, verbose=False): + playbook = os.path.join(CFG.settings['ansible_playbook_directory'], + 'playbooks/byo/openshift-cluster/upgrades/{}'.format(playbook)) + # TODO: Upgrade inventory for upgrade? inventory_file = generate_inventory(CFG.hosts) facts_env = os.environ.copy() diff --git a/utils/src/ooinstall/variants.py b/utils/src/ooinstall/variants.py index 571025543..9d98379bb 100644 --- a/utils/src/ooinstall/variants.py +++ b/utils/src/ooinstall/variants.py @@ -36,6 +36,7 @@ class Variant(object): # WARNING: Keep the versions ordered, most recent last: OSE = Variant('openshift-enterprise', 'OpenShift Enterprise', [ + Version('3.2', 'openshift-enterprise'), Version('3.1', 'openshift-enterprise'), Version('3.0', 'enterprise') ] @@ -43,6 +44,7 @@ OSE = Variant('openshift-enterprise', 'OpenShift Enterprise', AEP = Variant('atomic-enterprise', 'Atomic Enterprise Platform', [ + Version('3.2', 'atomic-enterprise'), Version('3.1', 'atomic-enterprise') ] ) @@ -74,4 +76,3 @@ def get_variant_version_combos(): for ver in variant.versions: combos.append((variant, ver)) return combos - diff --git a/utils/test/cli_installer_tests.py b/utils/test/cli_installer_tests.py index 6ba5ec1eb..524df08c4 100644 --- a/utils/test/cli_installer_tests.py +++ b/utils/test/cli_installer_tests.py @@ -1,6 +1,6 @@ # TODO: Temporarily disabled due to importing old code into openshift-ansible # repo. We will work on these over time. -# pylint: disable=bad-continuation,missing-docstring,no-self-use,invalid-name +# pylint: disable=bad-continuation,missing-docstring,no-self-use,invalid-name,too-many-lines import copy import os @@ -403,7 +403,7 @@ class UnattendedCliTests(OOCliFixture): self.assert_result(result, 0) load_facts_args = load_facts_mock.call_args[0] - self.assertEquals(os.path.join(self.work_dir, ".ansible/hosts"), + self.assertEquals(os.path.join(self.work_dir, "hosts"), load_facts_args[0]) self.assertEquals(os.path.join(self.work_dir, "playbooks/byo/openshift_facts.yml"), load_facts_args[1]) @@ -417,8 +417,8 @@ class UnattendedCliTests(OOCliFixture): env_vars['ANSIBLE_CONFIG'] == cli.DEFAULT_ANSIBLE_CONFIG) # Make sure we ran on the expected masters and nodes: - hosts = run_playbook_mock.call_args[0][0] - hosts_to_run_on = run_playbook_mock.call_args[0][1] + hosts = run_playbook_mock.call_args[0][1] + hosts_to_run_on = run_playbook_mock.call_args[0][2] self.assertEquals(3, len(hosts)) self.assertEquals(3, len(hosts_to_run_on)) @@ -441,7 +441,7 @@ class UnattendedCliTests(OOCliFixture): # Check the inventory file looks as we would expect: inventory = ConfigParser.ConfigParser(allow_no_value=True) - inventory.read(os.path.join(self.work_dir, '.ansible/hosts')) + inventory.read(os.path.join(self.work_dir, 'hosts')) self.assertEquals('bob', inventory.get('OSEv3:vars', 'ansible_ssh_user')) self.assertEquals('openshift-enterprise', @@ -480,11 +480,11 @@ class UnattendedCliTests(OOCliFixture): self.assertEquals('openshift-enterprise', written_config['variant']) # We didn't specify a version so the latest should have been assumed, # and written to disk: - self.assertEquals('3.1', written_config['variant_version']) + self.assertEquals('3.2', written_config['variant_version']) # Make sure the correct value was passed to ansible: inventory = ConfigParser.ConfigParser(allow_no_value=True) - inventory.read(os.path.join(self.work_dir, '.ansible/hosts')) + inventory.read(os.path.join(self.work_dir, 'hosts')) self.assertEquals('openshift-enterprise', inventory.get('OSEv3:vars', 'deployment_type')) @@ -512,7 +512,7 @@ class UnattendedCliTests(OOCliFixture): self.assertEquals('3.0', written_config['variant_version']) inventory = ConfigParser.ConfigParser(allow_no_value=True) - inventory.read(os.path.join(self.work_dir, '.ansible/hosts')) + inventory.read(os.path.join(self.work_dir, 'hosts')) self.assertEquals('enterprise', inventory.get('OSEv3:vars', 'deployment_type')) @@ -625,8 +625,8 @@ class UnattendedCliTests(OOCliFixture): self.assert_result(result, 0) # Make sure we ran on the expected masters and nodes: - hosts = run_playbook_mock.call_args[0][0] - hosts_to_run_on = run_playbook_mock.call_args[0][1] + hosts = run_playbook_mock.call_args[0][1] + hosts_to_run_on = run_playbook_mock.call_args[0][2] self.assertEquals(6, len(hosts)) self.assertEquals(6, len(hosts_to_run_on)) @@ -695,8 +695,8 @@ class UnattendedCliTests(OOCliFixture): self.assert_result(result, 0) # Make sure we ran on the expected masters and nodes: - hosts = run_playbook_mock.call_args[0][0] - hosts_to_run_on = run_playbook_mock.call_args[0][1] + hosts = run_playbook_mock.call_args[0][1] + hosts_to_run_on = run_playbook_mock.call_args[0][2] self.assertEquals(6, len(hosts)) self.assertEquals(6, len(hosts_to_run_on)) @@ -733,13 +733,13 @@ class AttendedCliTests(OOCliFixture): self._verify_config_hosts(written_config, 3) inventory = ConfigParser.ConfigParser(allow_no_value=True) - inventory.read(os.path.join(self.work_dir, '.ansible/hosts')) - self.assertEquals('False', - inventory.get('nodes', '10.0.0.1 openshift_schedulable')) - self.assertEquals(None, - inventory.get('nodes', '10.0.0.2')) - self.assertEquals(None, - inventory.get('nodes', '10.0.0.3')) + inventory.read(os.path.join(self.work_dir, 'hosts')) + self.assert_inventory_host_var(inventory, 'nodes', '10.0.0.1', + 'openshift_schedulable=False') + self.assert_inventory_host_var_unset(inventory, 'nodes', '10.0.0.2', + 'openshift_schedulable') + self.assert_inventory_host_var_unset(inventory, 'nodes', '10.0.0.3', + 'openshift_schedulable') # interactive with config file and some installed some uninstalled hosts @patch('ooinstall.openshift_ansible.run_main_playbook') @@ -851,15 +851,15 @@ class AttendedCliTests(OOCliFixture): self._verify_config_hosts(written_config, 6) inventory = ConfigParser.ConfigParser(allow_no_value=True) - inventory.read(os.path.join(self.work_dir, '.ansible/hosts')) - self.assertEquals('False', - inventory.get('nodes', '10.0.0.1 openshift_schedulable')) - self.assertEquals('False', - inventory.get('nodes', '10.0.0.2 openshift_schedulable')) - self.assertEquals('False', - inventory.get('nodes', '10.0.0.3 openshift_schedulable')) - self.assertEquals(None, - inventory.get('nodes', '10.0.0.4')) + inventory.read(os.path.join(self.work_dir, 'hosts')) + self.assert_inventory_host_var(inventory, 'nodes', '10.0.0.1', + 'openshift_schedulable=False') + self.assert_inventory_host_var(inventory, 'nodes', '10.0.0.2', + 'openshift_schedulable=False') + self.assert_inventory_host_var(inventory, 'nodes', '10.0.0.3', + 'openshift_schedulable=False') + self.assert_inventory_host_var_unset(inventory, 'nodes', '10.0.0.4', + 'openshift_schedulable') self.assertTrue(inventory.has_section('etcd')) self.assertEquals(3, len(inventory.items('etcd'))) @@ -892,13 +892,50 @@ class AttendedCliTests(OOCliFixture): self._verify_config_hosts(written_config, 5) inventory = ConfigParser.ConfigParser(allow_no_value=True) - inventory.read(os.path.join(self.work_dir, '.ansible/hosts')) - self.assertEquals('True', - inventory.get('nodes', '10.0.0.1 openshift_schedulable')) - self.assertEquals('True', - inventory.get('nodes', '10.0.0.2 openshift_schedulable')) - self.assertEquals('True', - inventory.get('nodes', '10.0.0.3 openshift_schedulable')) + inventory.read(os.path.join(self.work_dir, 'hosts')) + self.assert_inventory_host_var(inventory, 'nodes', '10.0.0.1', + 'openshift_schedulable=True') + self.assert_inventory_host_var(inventory, 'nodes', '10.0.0.2', + 'openshift_schedulable=True') + self.assert_inventory_host_var(inventory, 'nodes', '10.0.0.3', + 'openshift_schedulable=True') + + # Checks the inventory (as a ConfigParser) for the given host, host + # variable, and expected value. + def assert_inventory_host_var(self, inventory, section, host, variable): + # Config parser splits on the first "=", so we end up with: + # 'hostname key1' -> 'val1 key2=val2 key3=val3' + # + # Convert to something easier to test: + for (a, b) in inventory.items(section): + full_line = "%s=%s" % (a, b) + tokens = full_line.split() + if tokens[0] == host: + found = False + for token in tokens: + if token == variable: + found = True + continue + self.assertTrue("Unable to find %s in line: %s" % + (variable, full_line), found) + return + self.fail("unable to find host %s in inventory" % host) + + def assert_inventory_host_var_unset(self, inventory, section, host, variable): + # Config parser splits on the first "=", so we end up with: + # 'hostname key1' -> 'val1 key2=val2 key3=val3' + # + # Convert to something easier to test: + for (a, b) in inventory.items(section): + full_line = "%s=%s" % (a, b) + tokens = full_line.split() + if tokens[0] == host: + self.assertFalse(("%s=" % variable) in full_line, + msg='%s host variable was set: %s' % + (variable, full_line)) + return + self.fail("unable to find host %s in inventory" % host) + #interactive multimaster: attempting to use a master as the load balancer should fail: @patch('ooinstall.openshift_ansible.run_main_playbook') @@ -946,9 +983,9 @@ class AttendedCliTests(OOCliFixture): self._verify_config_hosts(written_config, 1) inventory = ConfigParser.ConfigParser(allow_no_value=True) - inventory.read(os.path.join(self.work_dir, '.ansible/hosts')) - self.assertEquals('True', - inventory.get('nodes', '10.0.0.1 openshift_schedulable')) + inventory.read(os.path.join(self.work_dir, 'hosts')) + self.assert_inventory_host_var(inventory, 'nodes', '10.0.0.1', + 'openshift_schedulable=True') #interactive 3.0 install confirm no HA hints @patch('ooinstall.openshift_ansible.run_main_playbook') @@ -960,15 +997,53 @@ class AttendedCliTests(OOCliFixture): cli_input = build_input(hosts=[ ('10.0.0.1', True, False)], ssh_user='root', - variant_num=2, + variant_num=3, confirm_facts='y') self.cli_args.append("install") result = self.runner.invoke(cli.cli, self.cli_args, input=cli_input) self.assert_result(result, 0) + print result.output self.assertTrue("NOTE: Add a total of 3 or more Masters to perform an HA installation." not in result.output) + @patch('ooinstall.openshift_ansible.run_main_playbook') + @patch('ooinstall.openshift_ansible.load_system_facts') + def test_gen_inventory(self, load_facts_mock, run_playbook_mock): + load_facts_mock.return_value = (MOCK_FACTS, 0) + run_playbook_mock.return_value = 0 + + cli_input = build_input(hosts=[ + ('10.0.0.1', True, False), + ('10.0.0.2', False, False), + ('10.0.0.3', False, False)], + ssh_user='root', + variant_num=1, + confirm_facts='y') + self.cli_args.append("install") + self.cli_args.append("--gen-inventory") + result = self.runner.invoke(cli.cli, self.cli_args, + input=cli_input) + self.assert_result(result, 0) + + self._verify_load_facts(load_facts_mock) + + # Make sure run playbook wasn't called: + self.assertEquals(0, len(run_playbook_mock.mock_calls)) + + written_config = read_yaml(self.config_file) + self._verify_config_hosts(written_config, 3) + + inventory = ConfigParser.ConfigParser(allow_no_value=True) + inventory.read(os.path.join(self.work_dir, 'hosts')) + self.assert_inventory_host_var(inventory, 'nodes', '10.0.0.1', + 'openshift_schedulable=False') + self.assert_inventory_host_var_unset(inventory, 'nodes', '10.0.0.2', + 'openshift_schedulable') + self.assert_inventory_host_var_unset(inventory, 'nodes', '10.0.0.3', + 'openshift_schedulable') + + # TODO: test with config file, attended add node # TODO: test with config file, attended new node already in config file # TODO: test with config file, attended new node already in config file, plus manually added nodes diff --git a/utils/test/fixture.py b/utils/test/fixture.py index d6222dfaa..1657d8f46 100644 --- a/utils/test/fixture.py +++ b/utils/test/fixture.py @@ -68,7 +68,7 @@ class OOCliFixture(OOInstallFixture): def _verify_load_facts(self, load_facts_mock): """ Check that we ran load facts with expected inputs. """ load_facts_args = load_facts_mock.call_args[0] - self.assertEquals(os.path.join(self.work_dir, ".ansible/hosts"), + self.assertEquals(os.path.join(self.work_dir, "hosts"), load_facts_args[0]) self.assertEquals(os.path.join(self.work_dir, "playbooks/byo/openshift_facts.yml"), @@ -81,8 +81,8 @@ class OOCliFixture(OOInstallFixture): def _verify_run_playbook(self, run_playbook_mock, exp_hosts_len, exp_hosts_to_run_on_len): """ Check that we ran playbook with expected inputs. """ - hosts = run_playbook_mock.call_args[0][0] - hosts_to_run_on = run_playbook_mock.call_args[0][1] + hosts = run_playbook_mock.call_args[0][1] + hosts_to_run_on = run_playbook_mock.call_args[0][2] self.assertEquals(exp_hosts_len, len(hosts)) self.assertEquals(exp_hosts_to_run_on_len, len(hosts_to_run_on)) @@ -133,8 +133,8 @@ class OOCliFixture(OOInstallFixture): self._verify_run_playbook(run_playbook_mock, exp_hosts_len, exp_hosts_to_run_on_len) # Make sure we ran on the expected masters and nodes: - hosts = run_playbook_mock.call_args[0][0] - hosts_to_run_on = run_playbook_mock.call_args[0][1] + hosts = run_playbook_mock.call_args[0][1] + hosts_to_run_on = run_playbook_mock.call_args[0][2] self.assertEquals(exp_hosts_len, len(hosts)) self.assertEquals(exp_hosts_to_run_on_len, len(hosts_to_run_on)) |