summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.tito/packages/openshift-ansible2
-rw-r--r--openshift-ansible.spec23
-rw-r--r--playbooks/common/openshift-cluster/upgrades/v3_1_to_v3_2/pre.yml10
-rw-r--r--roles/docker/defaults/main.yml2
-rw-r--r--roles/docker/tasks/main.yml41
-rw-r--r--roles/docker/vars/main.yml2
-rwxr-xr-xroles/docker_storage_setup/tasks/main.yml2
-rw-r--r--roles/lib_openshift_api/build/ansible/edit.py84
-rw-r--r--roles/lib_openshift_api/build/ansible/obj.py7
-rwxr-xr-xroles/lib_openshift_api/build/generate.py12
-rw-r--r--roles/lib_openshift_api/build/src/base.py55
-rw-r--r--roles/lib_openshift_api/build/src/edit.py49
-rwxr-xr-xroles/lib_openshift_api/build/test/edit.yml53
-rw-r--r--roles/lib_openshift_api/build/test/files/dc.yml9
-rw-r--r--roles/lib_openshift_api/library/oc_edit.py619
-rw-r--r--roles/lib_openshift_api/library/oc_obj.py219
-rw-r--r--roles/lib_openshift_api/library/oc_secret.py212
-rw-r--r--roles/lib_yaml_editor/build/ansible/yedit.py9
-rwxr-xr-xroles/lib_yaml_editor/build/generate.py9
-rw-r--r--roles/lib_yaml_editor/build/src/base.py8
-rw-r--r--roles/lib_yaml_editor/build/src/yedit.py157
-rw-r--r--roles/lib_yaml_editor/build/test/foo.yml2
-rw-r--r--roles/lib_yaml_editor/library/yedit.py174
-rw-r--r--roles/openshift_cli/meta/main.yml1
-rw-r--r--roles/openshift_docker/meta/main.yml1
-rw-r--r--roles/openshift_docker_facts/tasks/main.yml16
-rw-r--r--roles/openshift_docker_facts/vars/main.yml2
-rw-r--r--roles/openshift_facts/tasks/main.yml2
-rw-r--r--roles/openshift_master/meta/main.yml1
-rw-r--r--roles/openshift_master_cluster/tasks/main.yml4
-rw-r--r--roles/os_zabbix/vars/template_heartbeat.yml5
-rw-r--r--test/env-setup2
-rwxr-xr-xtest/units/yedit_test.py68
-rw-r--r--utils/src/ooinstall/cli_installer.py79
-rw-r--r--utils/src/ooinstall/oo_config.py2
-rw-r--r--utils/src/ooinstall/openshift_ansible.py19
-rw-r--r--utils/src/ooinstall/variants.py3
-rw-r--r--utils/test/cli_installer_tests.py153
-rw-r--r--utils/test/fixture.py10
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))