Browse Source

Merge branch 'master' into vsphere-39-svc-datastore-fix

Scott Dodson 7 years ago
parent
commit
bc6bd0ba46
100 changed files with 1681 additions and 483 deletions
  1. 1 1
      .tito/packages/openshift-ansible
  2. 54 35
      inventory/hosts.example
  3. 8 3
      inventory/hosts.grafana.example
  4. 125 1
      openshift-ansible.spec
  5. 10 0
      playbooks/aws/openshift-cluster/uninstall.yml
  6. 19 0
      playbooks/aws/openshift-cluster/uninstall_masters.yml
  7. 18 0
      playbooks/aws/openshift-cluster/uninstall_nodes.yml
  8. 1 0
      playbooks/aws/provisioning-inventory.example.ini
  9. 0 3
      playbooks/byo/openshift-cluster/upgrades/docker/upgrade.yml
  10. 24 0
      playbooks/cluster-operator/aws/components.yml
  11. 21 0
      playbooks/cluster-operator/aws/uninstall_infrastructure.yml
  12. 28 3
      playbooks/common/openshift-cluster/upgrades/docker/docker_upgrade.yml
  13. 8 1
      playbooks/common/openshift-cluster/upgrades/post_control_plane.yml
  14. 5 0
      playbooks/common/openshift-cluster/upgrades/pre/config.yml
  15. 1 1
      playbooks/common/openshift-cluster/upgrades/pre/verify_cluster.yml
  16. 29 0
      playbooks/common/openshift-cluster/upgrades/pre/version_override.yml
  17. 4 0
      playbooks/common/openshift-cluster/upgrades/upgrade_control_plane.yml
  18. 0 7
      playbooks/common/openshift-cluster/upgrades/v3_10/validator.yml
  19. 0 2
      playbooks/common/openshift-cluster/upgrades/v3_8/upgrade.yml
  20. 0 2
      playbooks/common/openshift-cluster/upgrades/v3_8/upgrade_control_plane.yml
  21. 0 7
      playbooks/common/openshift-cluster/upgrades/v3_8/validator.yml
  22. 38 37
      playbooks/common/openshift-cluster/upgrades/v3_9/upgrade_control_plane.yml
  23. 0 7
      playbooks/common/openshift-cluster/upgrades/v3_9/validator.yml
  24. 3 1
      playbooks/container-runtime/config.yml
  25. 3 1
      playbooks/container-runtime/setup_storage.yml
  26. 2 2
      playbooks/init/main.yml
  27. 1 1
      playbooks/init/version.yml
  28. 6 0
      playbooks/openshift-etcd/certificates.yml
  29. 6 0
      playbooks/openshift-etcd/config.yml
  30. 6 0
      playbooks/openshift-etcd/embedded2external.yml
  31. 6 0
      playbooks/openshift-etcd/migrate.yml
  32. 6 0
      playbooks/openshift-etcd/redeploy-ca.yml
  33. 6 0
      playbooks/openshift-etcd/redeploy-certificates.yml
  34. 6 0
      playbooks/openshift-etcd/restart.yml
  35. 4 1
      playbooks/openshift-etcd/scaleup.yml
  36. 3 1
      playbooks/openshift-etcd/upgrade.yml
  37. 5 0
      playbooks/openshift-glusterfs/config.yml
  38. 4 5
      playbooks/openshift-glusterfs/private/config.yml
  39. 8 0
      playbooks/openshift-glusterfs/private/uninstall.yml
  40. 5 0
      playbooks/openshift-glusterfs/registry.yml
  41. 4 0
      playbooks/openshift-glusterfs/uninstall.yml
  42. 5 0
      playbooks/openshift-grafana/config.yml
  43. 5 0
      playbooks/openshift-hosted/config.yml
  44. 5 0
      playbooks/openshift-hosted/deploy_registry.yml
  45. 5 0
      playbooks/openshift-hosted/deploy_router.yml
  46. 0 4
      playbooks/openshift-hosted/private/openshift_hosted_registry.yml
  47. 0 4
      playbooks/openshift-hosted/private/openshift_hosted_router.yml
  48. 5 0
      playbooks/openshift-hosted/redeploy-registry-certificates.yml
  49. 5 0
      playbooks/openshift-hosted/redeploy-router-certificates.yml
  50. 5 0
      playbooks/openshift-loadbalancer/config.yml
  51. 5 0
      playbooks/openshift-logging/config.yml
  52. 5 0
      playbooks/openshift-management/config.yml
  53. 4 0
      playbooks/openshift-master/enable_bootstrap.yml
  54. 3 5
      playbooks/openshift-master/private/config.yml
  55. 7 0
      playbooks/openshift-master/private/enable_bootstrap.yml
  56. 20 0
      playbooks/openshift-master/private/tasks/enable_bootstrap.yml
  57. 3 19
      playbooks/openshift-master/private/tasks/restart_hosts.yml
  58. 5 1
      playbooks/openshift-master/private/validate_restart.yml
  59. 6 0
      playbooks/openshift-metrics/config.yml
  60. 6 0
      playbooks/openshift-nfs/config.yml
  61. 7 0
      playbooks/openshift-node/private/image_prep.yml
  62. 6 0
      playbooks/openshift-prometheus/config.yml
  63. 6 0
      playbooks/openshift-provisioners/config.yml
  64. 6 0
      playbooks/openshift-service-catalog/config.yml
  65. 5 0
      playbooks/openshift-web-console/config.yml
  66. 5 2
      playbooks/openshift-web-console/private/config.yml
  67. 32 0
      playbooks/openstack/advanced-configuration.md
  68. 1 1
      playbooks/openstack/inventory.py
  69. 1 2
      playbooks/openstack/openshift-cluster/install.yml
  70. 10 4
      playbooks/openstack/openshift-cluster/provision.yml
  71. 10 1
      playbooks/openstack/sample-inventory/group_vars/all.yml
  72. 3 1
      playbooks/prerequisites.yml
  73. 7 2
      roles/container_runtime/defaults/main.yml
  74. 12 8
      roles/container_runtime/tasks/docker_upgrade_check.yml
  75. 3 3
      roles/container_runtime/tasks/package_docker.yml
  76. 1 1
      roles/container_runtime/templates/crio.conf.j2
  77. 1 1
      roles/etcd/defaults/main.yaml
  78. 4 0
      roles/kuryr/tasks/master.yaml
  79. 65 0
      roles/lib_openshift/action_plugins/conditional_set_fact.py
  80. 0 74
      roles/lib_openshift/library/conditional_set_fact.py
  81. 48 12
      roles/lib_openshift/library/oc_adm_ca_server_cert.py
  82. 55 12
      roles/lib_openshift/library/oc_adm_csr.py
  83. 48 12
      roles/lib_openshift/library/oc_adm_manage_node.py
  84. 48 12
      roles/lib_openshift/library/oc_adm_policy_group.py
  85. 48 12
      roles/lib_openshift/library/oc_adm_policy_user.py
  86. 48 12
      roles/lib_openshift/library/oc_adm_registry.py
  87. 48 12
      roles/lib_openshift/library/oc_adm_router.py
  88. 48 12
      roles/lib_openshift/library/oc_clusterrole.py
  89. 48 12
      roles/lib_openshift/library/oc_configmap.py
  90. 48 12
      roles/lib_openshift/library/oc_edit.py
  91. 48 12
      roles/lib_openshift/library/oc_env.py
  92. 48 12
      roles/lib_openshift/library/oc_group.py
  93. 48 12
      roles/lib_openshift/library/oc_image.py
  94. 48 12
      roles/lib_openshift/library/oc_label.py
  95. 48 12
      roles/lib_openshift/library/oc_obj.py
  96. 48 12
      roles/lib_openshift/library/oc_objectvalidator.py
  97. 48 12
      roles/lib_openshift/library/oc_process.py
  98. 48 12
      roles/lib_openshift/library/oc_project.py
  99. 48 12
      roles/lib_openshift/library/oc_pvc.py
  100. 0 0
      roles/lib_openshift/library/oc_route.py

+ 1 - 1
.tito/packages/openshift-ansible

@@ -1 +1 @@
-3.9.0-0.42.0 ./
+3.9.0-0.47.0 ./

+ 54 - 35
inventory/hosts.example

@@ -1,4 +1,25 @@
-# This is an example of an OpenShift-Ansible host inventory
+# This is an example of an OpenShift-Ansible host inventory that provides the
+# minimum recommended configuration for production use. This includes 3 masters,
+# two infra nodes, two compute nodes, and an haproxy load balancer to load
+# balance traffic to the API servers. For a truly production environment you
+# should use an external load balancing solution that itself is highly available.
+
+[masters]
+ose3-master[1:3].test.example.com
+
+[etcd]
+ose3-master[1:3].test.example.com
+
+[nodes]
+ose3-master[1:3].test.example.com
+ose3-infra[1:2].test.example.com openshift_node_labels="{'region': 'infra', 'zone': 'default'}"
+ose3-node[1:2].test.example.com openshift_node_labels="{'region': 'primary', 'zone': 'default'}"
+
+[nfs]
+ose3-master1.test.example.com
+
+[lb]
+ose3-lb.test.example.com
 
 # Create an OSEv3 group that contains the masters and nodes groups
 [OSEv3:children]
@@ -8,12 +29,10 @@ etcd
 lb
 nfs
 
-# Set variables common for all OSEv3 hosts
 [OSEv3:vars]
-# Enable unsupported configurations, things that will yield a partially
-# functioning cluster but would not be supported for production use
-#openshift_enable_unsupported_configurations=false
-
+###############################################################################
+# Common/ Required configuration variables follow                             #
+###############################################################################
 # SSH user, this user should allow ssh based auth without requiring a
 # password. If using ssh key based auth, then the key should be managed by an
 # ssh agent.
@@ -23,9 +42,6 @@ ansible_user=root
 # user must be configured for passwordless sudo
 #ansible_become=yes
 
-# Debug level for all OpenShift components (Defaults to 2)
-debug_level=2
-
 # Specify the deployment type. Valid values are origin and openshift-enterprise.
 openshift_deployment_type=origin
 #openshift_deployment_type=openshift-enterprise
@@ -37,6 +53,23 @@ openshift_deployment_type=origin
 # release.
 openshift_release=v3.7
 
+# default subdomain to use for exposed routes, you should have wildcard dns
+# for *.apps.test.example.com that points at your infra nodes which will run
+# your router
+openshift_master_default_subdomain=apps.test.example.com
+
+#Set cluster_hostname to point at your load balancer
+openshift_master_cluster_hostname=ose3-lb.test.example.com
+
+
+
+###############################################################################
+# Additional configuration variables follow                                   #
+###############################################################################
+
+# Debug level for all OpenShift components (Defaults to 2)
+debug_level=2
+
 # Specify an exact container image tag to install or configure.
 # WARNING: This value will be used for all hosts in containerized environments, even those that have another version installed.
 # This could potentially trigger an upgrade and downtime, so be careful with modifying this value after the cluster is set up.
@@ -202,7 +235,7 @@ openshift_release=v3.7
 #osm_etcd_image=rhel7/etcd
 
 # htpasswd auth
-openshift_master_identity_providers=[{'name': 'htpasswd_auth', 'login': 'true', 'challenge': 'true', 'kind': 'HTPasswdPasswordIdentityProvider', 'filename': '/etc/origin/master/htpasswd'}]
+#openshift_master_identity_providers=[{'name': 'htpasswd_auth', 'login': 'true', 'challenge': 'true', 'kind': 'HTPasswdPasswordIdentityProvider', 'filename': '/etc/origin/master/htpasswd'}]
 # Defining htpasswd users
 #openshift_master_htpasswd_users={'user1': '<pre-hashed password>', 'user2': '<pre-hashed password>'}
 # or
@@ -336,9 +369,6 @@ openshift_master_identity_providers=[{'name': 'htpasswd_auth', 'login': 'true',
 # Configure api server arguments
 #osm_api_server_args={'max-requests-inflight': ['400']}
 
-# default subdomain to use for exposed routes
-#openshift_master_default_subdomain=apps.test.example.com
-
 # additional cors origins
 #osm_custom_cors_origins=['foo.example.com', 'bar.example.com']
 
@@ -838,8 +868,14 @@ openshift_master_identity_providers=[{'name': 'htpasswd_auth', 'login': 'true',
 # interface other than the default network interface.
 #openshift_set_node_ip=True
 
-# Configure dnsIP in the node config
-#openshift_dns_ip=172.30.0.1
+# Configure dnsIP in the node config.
+# This setting overrides the bind IP address used by each node's dnsmasq.
+# By default, this value is set to the IP which ansible uses to connect to the node.
+# Only update this variable if you need to bind dnsmasq on a different interface
+#
+# Example:
+# [nodes]
+# node.example.com openshift_dns_ip=172.30.0.1
 
 # Configure node kubelet arguments. pods-per-core is valid in OpenShift Origin 1.3 or OpenShift Container Platform 3.3 and later.
 #openshift_node_kubelet_args={'pods-per-core': ['10'], 'max-pods': ['250'], 'image-gc-high-threshold': ['85'], 'image-gc-low-threshold': ['80']}
@@ -1107,23 +1143,6 @@ openshift_master_identity_providers=[{'name': 'htpasswd_auth', 'login': 'true',
 #openshift_master_open_ports=[{"service":"svc1","port":"11/tcp"}]
 #openshift_node_open_ports=[{"service":"svc2","port":"12-13/tcp"},{"service":"svc3","port":"14/udp"}]
 
-# host group for masters
-[masters]
-ose3-master[1:3]-ansible.test.example.com
-
-[etcd]
-ose3-etcd[1:3]-ansible.test.example.com
-
-# NOTE: Containerized load balancer hosts are not yet supported, if using a global
-# containerized=true host variable we must set to false.
-[lb]
-ose3-lb-ansible.test.example.com containerized=false
-
-# NOTE: Currently we require that masters be part of the SDN which requires that they also be nodes
-[nodes]
-# masters should be schedulable to run web console pods
-ose3-master[1:3]-ansible.test.example.com openshift_schedulable=True
-ose3-node[1:2]-ansible.test.example.com openshift_node_labels="{'region': 'primary', 'zone': 'default'}"
-
-[nfs]
-ose3-nfs-ansible.test.example.com
+# Enable unsupported configurations, things that will yield a partially
+# functioning cluster but would not be supported for production use
+#openshift_enable_unsupported_configurations=false

+ 8 - 3
inventory/hosts.grafana.example

@@ -4,9 +4,14 @@ nodes
 
 [OSEv3:vars]
 # Grafana Configuration
-#gf_datasource_name="example"
-#gf_prometheus_namespace="openshift-metrics"
-#gf_oauth=true
+#grafana_namespace=grafana
+#grafana_user=grafana
+#grafana_password=grafana
+#grafana_datasource_name="example"
+#grafana_prometheus_namespace="openshift-metrics"
+#grafana_prometheus_sa=prometheus
+#grafana_node_exporter=false
+#grafana_graph_granularity="2m"
 
 [masters]
 master

+ 125 - 1
openshift-ansible.spec

@@ -10,7 +10,7 @@
 
 Name:           openshift-ansible
 Version:        3.9.0
-Release:        0.42.0%{?dist}
+Release:        0.47.0%{?dist}
 Summary:        Openshift and Atomic Enterprise Ansible
 License:        ASL 2.0
 URL:            https://github.com/openshift/openshift-ansible
@@ -201,6 +201,130 @@ Atomic OpenShift Utilities includes
 
 
 %changelog
+* Tue Feb 20 2018 Justin Pierce <jupierce@redhat.com> 3.9.0-0.47.0
+- Update API healthz check to use uri module (mkhan@redhat.com)
+- fixed an oo_filter plugin lib_utils_oo_has_no_matching_selector to do set
+  comparison (mwoodson@redhat.com)
+- Grafana roles updates. (mrsiano@gmail.com)
+- add deprovision playbook for cluster-operator infrastructure
+  (jdiaz@redhat.com)
+- Add tox test to check for invalid playbook include (rteague@redhat.com)
+- Change openshift.common.hostname to inventory_hostname (mgugino@redhat.com)
+- Fix openshift-webconsole version check (mgugino@redhat.com)
+- add master deprovisioning (jdiaz@redhat.com)
+- Adding file locking to yedit. (kwoodson@redhat.com)
+- Log troubleshooting info when console install fails (spadgett@redhat.com)
+- CRI-O: use /var/run/crio/crio.sock for >=3.9 (gscrivan@redhat.com)
+- Fix pvc template by replacing None by lowercase none (toj315@gmail.com)
+- GlusterFS: Fix uninstall regression (jarrpa@redhat.com)
+- Add prometheus reader role for lightweight privileges. (mrsiano@gmail.com)
+- docker_image_availability: encode error message (vrutkovs@redhat.com)
+- Tweak things based on feedback (sdodson@redhat.com)
+- Update example inventory to drive required hostgroups to the top
+  (sdodson@redhat.com)
+
+* Mon Feb 19 2018 Justin Pierce <jupierce@redhat.com> 3.9.0-0.46.0
+- Tolerate OVS 2.6 in 3.10 as well (sdodson@redhat.com)
+- hosts.example: openshift_dns_ip should be node-specific (vrutkovs@redhat.com)
+- Add target mount for gluster block (m.judeikis@gmail.com)
+- Allow for overriding hosted registry_url variables (rteague@redhat.com)
+- Link to etcd v3 migration docs rather than suggesting dangerous things
+  (sdodson@redhat.com)
+- Run openshift_version for image prep (mgugino@redhat.com)
+- Remove redundant openshift_hosted_registry_network_default
+  (mgugino@redhat.com)
+- Correct the usage of bool and str (ghuang@redhat.com)
+- kernel module loading fix (m.judeikis@gmail.com)
+- add steps in bootstrap playbook to handle updating aws.conf file
+  (jdiaz@redhat.com)
+- Add cloud config variables to the sample inventory (nelluri@redhat.com)
+- Run init/facts for docker upgrade (mgugino@redhat.com)
+- quick installer: remove UPGRADE_MAPPINGS (vrutkovs@redhat.com)
+- Update quick installer to support 3.9 and 3.8 (vrutkovs@redhat.com)
+- Updating deprecation variable check to use a module for cleaner output and
+  use run_once to limit to one host. Add flag to skip dep check if desired
+  (ewolinet@redhat.com)
+- Patch only if the file exists, otherwise we should copy the file in
+  (ewolinet@redhat.com)
+- Add vsphere section for openshift_node_kubelet_args_dict (ghuang@redhat.com)
+- Correctly comparing against the current configmap when making es configmap
+  patches (ewolinet@redhat.com)
+- add uninstall playbooks for compute/infra scale groups (jdiaz@redhat.com)
+- Adding ability to pass content and create files from content.
+  (kwoodson@redhat.com)
+- Bug 1541946- waiting for master reboot now works behind bastion
+  (fabian@fabianism.us)
+
+* Thu Feb 15 2018 Justin Pierce <jupierce@redhat.com> 3.9.0-0.45.0
+- 
+
+* Thu Feb 15 2018 Justin Pierce <jupierce@redhat.com> 3.9.0-0.44.0
+- 
+
+* Thu Feb 15 2018 Justin Pierce <jupierce@redhat.com> 3.9.0-0.43.0
+- Changing conditional_set_fact from module to action_plugin since it does not
+  need to access hosts to be effective and to reduce playbook output
+  (ewolinet@redhat.com)
+- Revert "Bug 1512825 - add mux pod failed for Serial number 02 has already
+  been issued" (mkhan@redhat.com)
+- Fix metadata access in OpenStack inventory (tomas@sedovic.cz)
+- Adding ability to yedit json files. (kwoodson@redhat.com)
+- Simplify double upgrade version logic (mgugino@redhat.com)
+- Whenever we create a new es node ignore health checks, changing prometheus pw
+  gen for increased secret idempotency (ewolinet@redhat.com)
+- oc_adm_csr: Add fail_on_timeout parameter which causes module to fail when
+  timeout was reached. (abutcher@redhat.com)
+- Adding missing template (ewolinet@redhat.com)
+- Move installation of packages before container_runtime to ensure bind mounts
+  are avaialable. (kwoodson@redhat.com)
+- Use curl --noproxy option for internal apiserver access (takayoshi@gmail.com)
+- Revert openshift_version to previous state (mgugino@redhat.com)
+- Add openshift_gcp_multizone bool (mgugino@redhat.com)
+- Invert logic to decide when to re-deploy certs (sdodson@redhat.com)
+- etcd_scaleup: use inventory_hostname when etcd ca host is being picked
+  (vrutkovs@redhat.com)
+- Fix docker_upgrade variable (mgugino@redhat.com)
+- Fix gcp variable warnings (mgugino@redhat.com)
+- Disable console install when not 3.9 or newer (spadgett@redhat.com)
+- Fix etcd scaleup plays (mgugino@redhat.com)
+- Add playbook to install components for cluster operator (cewong@redhat.com)
+- Remove cluster_facts.yml from the install.yml (tomas@sedovic.cz)
+- Allow for blank StorageClass in PVC creation (jarrpa@redhat.com)
+- Add service catalog to be upgraded (jpeeler@redhat.com)
+- Remove node start from bootstrap.yml. (abutcher@redhat.com)
+- Restart systemd-hostnamed before restarting NetworkManager in node user-data.
+  (abutcher@redhat.com)
+- additional mounts: specify 'type' in container_runtime_crio_additional_mounts
+  (vrutkovs@redhat.com)
+- Fix openshift_openstack_provision_user_commands (bdobreli@redhat.com)
+- origin-dns: make sure cluster.local DNS server is listed first
+  (vrutkovs@redhat.com)
+- Fix OpenStack playbooks (tomas@sedovic.cz)
+- Backport changes for glusterfs, heketi, s3 and block templates
+  (sarumuga@redhat.com)
+- Fix indentation to make yamllint happy (vrutkovs@redhat.com)
+- Use r_etcd_common_etcdctl_command instead of hardcoded binary name to support
+  containerized upgrade (vrutkovs@redhat.com)
+- Verify that requested services have schedulable nodes matching the selectors
+  (vrutkovs@redhat.com)
+- Normalize the time we wait for pods to 5s * 60 retries (sdodson@redhat.com)
+- Pause for console rollout (spadgett@redhat.com)
+- Fix wording (bdobreli@redhat.com)
+- Fix cloud init runcmd templating (bdobreli@redhat.com)
+- Note ignored Heat user data changes for openstack (bdobreli@redhat.com)
+- Clarify the ansible playbook vs cloud-init (bdobreli@redhat.com)
+- Fix openstack cloud-init runcmd templating (bdobreli@redhat.com)
+- [openstack] custom user commands for cloud-init (bdobreli@redhat.com)
+- Limit host scope during plays (mgugino@redhat.com)
+- Fix upgrade-control plane post_control_plane.yml (mgugino@redhat.com)
+- erase data only if variable is set. fix block indentatation
+  (sarumuga@redhat.com)
+- uninstall playbook for GlusterFS (sarumuga@redhat.com)
+- Removing prefix and replacing with cidr, pool_start and pool_end variables.
+  (mbruzek@gmail.com)
+- Make node start options configurable (celebdor@gmail.com)
+- Support master node high availability (jihoon.o@samsung.com)
+
 * Fri Feb 09 2018 Justin Pierce <jupierce@redhat.com> 3.9.0-0.42.0
 - xPaaS v1.4.8 for v3.7 (sdodson@redhat.com)
 - xPaaS v1.4.8-1 for v3.8 (sdodson@redhat.com)

+ 10 - 0
playbooks/aws/openshift-cluster/uninstall.yml

@@ -0,0 +1,10 @@
+---
+- import_playbook: uninstall_nodes.yml
+
+- import_playbook: uninstall_masters.yml
+
+- import_playbook: uninstall_s3.yml
+
+- import_playbook: uninstall_elb.yml
+
+- import_playbook: uninstall_prerequisites.yml

+ 19 - 0
playbooks/aws/openshift-cluster/uninstall_masters.yml

@@ -0,0 +1,19 @@
+---
+- name: Alert user to variables needed
+  hosts: localhost
+  tasks:
+  - name: Alert user to variables needed - clusterid
+    debug:
+      msg: "openshift_aws_clusterid={{ openshift_aws_clusterid }}"
+
+  - name: Alert user to variables needed - region
+    debug:
+      msg: "openshift_aws_region={{ openshift_aws_region }}"
+
+- name: Delete the master node group
+  hosts: localhost
+  tasks:
+  - name: delete masters
+    import_role:
+      name: openshift_aws
+      tasks_from: uninstall_masters.yml

+ 18 - 0
playbooks/aws/openshift-cluster/uninstall_nodes.yml

@@ -0,0 +1,18 @@
+---
+- name: delete the node scale groups
+  hosts: localhost
+  connection: local
+  gather_facts: yes
+  tasks:
+  - name: Alert user to variables needed - clusterid
+    debug:
+      msg: "openshift_aws_clusterid={{ openshift_aws_clusterid }}"
+
+  - name: Alert user to variables needed - region
+    debug:
+      msg: "openshift_aws_region={{ openshift_aws_region }}"
+
+  - name: delete the node groups
+    import_role:
+      name: openshift_aws
+      tasks_from: uninstall_nodes.yml

+ 1 - 0
playbooks/aws/provisioning-inventory.example.ini

@@ -9,6 +9,7 @@ etcd
 ################################################################################
 # openshift_deployment_type is required for installation
 openshift_deployment_type=origin
+openshift_cloudprovider_kind=aws
 
 openshift_master_bootstrap_enabled=True
 openshift_master_api_port=443

+ 0 - 3
playbooks/byo/openshift-cluster/upgrades/docker/upgrade.yml

@@ -1,5 +1,2 @@
 ---
-# Playbook to upgrade Docker to the max allowable version for an OpenShift cluster.
-- import_playbook: ../../../../init/evaluate_groups.yml
-
 - import_playbook: ../../../../common/openshift-cluster/upgrades/docker/docker_upgrade.yml

+ 24 - 0
playbooks/cluster-operator/aws/components.yml

@@ -0,0 +1,24 @@
+---
+- name: Alert user to variables needed
+  hosts: localhost
+  tasks:
+  - name: Alert user to variables needed - clusterid
+    debug:
+      msg: "openshift_aws_clusterid={{ openshift_aws_clusterid | default('default') }}"
+
+  - name: Alert user to variables needed - region
+    debug:
+      msg: "openshift_aws_region={{ openshift_aws_region | default('us-east-1') }}"
+
+- name: Setup the master node group
+  hosts: localhost
+  tasks:
+  - import_role:
+      name: openshift_aws
+      tasks_from: setup_master_group.yml
+
+- name: run the init
+  import_playbook: ../../init/main.yml
+
+- name: Include the components playbook to finish the hosted configuration
+  import_playbook: ../../common/private/components.yml

+ 21 - 0
playbooks/cluster-operator/aws/uninstall_infrastructure.yml

@@ -0,0 +1,21 @@
+---
+- name: Deprovision infrastructure
+  hosts: localhost
+  tasks:
+  - name: Alert user to variables needed - clusterid
+    debug:
+      msg: "openshift_aws_clusterid={{ openshift_aws_clusterid }}"
+
+  - name: Alert user to variables needed - region
+    debug:
+      msg: "openshift_aws_region={{ openshift_aws_region }}"
+
+- import_playbook: ../../aws/openshift-cluster/uninstall_elb.yml
+
+- import_playbook: ../../aws/openshift-cluster/uninstall_s3.yml
+
+- import_playbook: ../../aws/openshift-cluster/uninstall_sec_group.yml
+
+- import_playbook: ../../aws/openshift-cluster/uninstall_ssh_keypair.yml
+
+- import_playbook: ../../aws/openshift-cluster/uninstall_vpc.yml

+ 28 - 3
playbooks/common/openshift-cluster/upgrades/docker/docker_upgrade.yml

@@ -7,6 +7,26 @@
 
 - import_playbook: ../initialize_nodes_to_upgrade.yml
 
+- import_playbook: ../../../../init/basic_facts.yml
+  vars:
+    l_init_fact_hosts: "oo_masters_to_config:oo_etcd_to_config:oo_lb_to_config:oo_nodes_to_upgrade"
+
+- import_playbook: ../../../../init/cluster_facts.yml
+  vars:
+    l_init_fact_hosts: "oo_masters_to_config:oo_etcd_to_config:oo_lb_to_config:oo_nodes_to_upgrade"
+
+# We need version for sanity_checks, but we don't need to actually check if
+# packages/images are available because we're not install any origin components.
+- import_playbook: ../../../../init/version.yml
+  vars:
+    l_openshift_version_set_hosts: "oo_etcd_to_config:oo_nodes_to_upgrade:oo_masters_to_config:!oo_first_master"
+    l_openshift_version_check_hosts: "all:!all"
+
+# Ensure inventory sanity_checks are run.
+- import_playbook: ../../../../init/sanity_checks.yml
+  vars:
+    l_sanity_check_hosts: "{{ groups['oo_nodes_to_upgrade'] | union(groups['oo_masters_to_config']) | union(groups['oo_etcd_to_config']) }}"
+
 - name: Check for appropriate Docker versions
   hosts: oo_masters_to_config:oo_nodes_to_upgrade:oo_etcd_to_config
   roles:
@@ -19,7 +39,7 @@
   - import_role:
       name: container_runtime
       tasks_from: docker_upgrade_check.yml
-    when: docker_upgrade is not defined or docker_upgrade | bool
+    when: docker_upgrade | default(True) | bool
 
 
 # If a node fails, halt everything, the admin will need to clean up and we
@@ -56,7 +76,10 @@
       --force --delete-local-data --ignore-daemonsets
       --timeout={{ openshift_upgrade_nodes_drain_timeout | default(0) }}s
     delegate_to: "{{ groups.oo_first_master.0 }}"
-    when: l_docker_upgrade is defined and l_docker_upgrade | bool and inventory_hostname in groups.oo_nodes_to_upgrade
+    when:
+    - l_docker_upgrade is defined
+    - l_docker_upgrade | bool
+    - inventory_hostname in groups.oo_nodes_to_upgrade
     register: l_docker_upgrade_drain_result
     until: not (l_docker_upgrade_drain_result is failed)
     retries: "{{ 1 if ( openshift_upgrade_nodes_drain_timeout | default(0) | int ) == 0 else 0 }}"
@@ -66,7 +89,9 @@
     - openshift_upgrade_nodes_drain_timeout | default(0) | int == 0
 
   - include_tasks: tasks/upgrade.yml
-    when: l_docker_upgrade is defined and l_docker_upgrade | bool
+    when:
+    - l_docker_upgrade is defined
+    - l_docker_upgrade | bool
 
   - name: Set node schedulability
     oc_adm_manage_node:

+ 8 - 1
playbooks/common/openshift-cluster/upgrades/post_control_plane.yml

@@ -130,7 +130,7 @@
     # Step 2: Set a fact to be used to determine if we should run the redeploy of registry certs
     - name: set a fact to include the registry certs playbook if needed
       set_fact:
-        openshift_hosted_rollout_certs_and_registry: "{{ cert_output.rc == 0  }}"
+        openshift_hosted_rollout_certs_and_registry: "{{ cert_output.rc != 0  }}"
 
 # Run the redeploy certs based upon the certificates. Defaults to False for insecure registries
 - when: (hostvars[groups.oo_first_master.0].openshift_hosted_rollout_certs_and_registry | default(False)) | bool
@@ -165,3 +165,10 @@
       msg: "WARNING the shared-resource-viewer role could not be upgraded to 3.6 spec because it's marked protected, please see https://bugzilla.redhat.com/show_bug.cgi?id=1493213"
     when:
     - __shared_resource_viewer_protected | default(false)
+
+- name: Upgrade Service Catalog
+  hosts: oo_first_master
+  roles:
+  - role: openshift_service_catalog
+    when:
+    - openshift_enable_service_catalog | default(true) | bool

+ 5 - 0
playbooks/common/openshift-cluster/upgrades/pre/config.yml

@@ -51,6 +51,10 @@
     # l_openshift_version_set_hosts is passed via upgrade_control_plane.yml
     # l_openshift_version_check_hosts is passed via upgrade_control_plane.yml
 
+# version_override will set various version-related variables during a double upgrade.
+- import_playbook: version_override.yml
+  when: l_double_upgrade_cp | default(False)
+
 - import_playbook: verify_cluster.yml
 
 # If we're only upgrading nodes, we need to ensure masters are already upgraded
@@ -79,3 +83,4 @@
   - import_role:
       name: container_runtime
       tasks_from: docker_upgrade_check.yml
+    when: docker_upgrade | default(True) | bool

+ 1 - 1
playbooks/common/openshift-cluster/upgrades/pre/verify_cluster.yml

@@ -55,7 +55,7 @@
     register: _storage_backend
 
   - fail:
-      msg: "Storage backend in /etc/origin/master/master-config.yaml must be set to 'etcd3' before the upgrade can continue"
+      msg: "The cluster must be migrated to etcd v3 prior to upgrading to 3.7. Please see https://docs.openshift.com/container-platform/3.7/install_config/upgrading/migrating_etcd.html"
     when:
     # assuming the master-config.yml is properly configured, i.e. the value is a list
     - _storage_backend.result | default([], true) | length == 0 or _storage_backend.result[0] != "etcd3"

+ 29 - 0
playbooks/common/openshift-cluster/upgrades/pre/version_override.yml

@@ -0,0 +1,29 @@
+---
+# This playbook overrides normal version setting during double upgrades.
+
+- name: Set proper version values for upgrade
+  hosts: "{{ l_version_override_hosts | default('all:!all') }}"
+  tasks:
+    - set_fact:
+        # All of these will either have been set by openshift_version or
+        # provided by the user; we need to save these for later.
+        l_double_upgrade_saved_version: "{{ openshift_version }}"
+        l_double_upgrade_saved_release: "{{ openshift_release | default(openshift_upgrade_target) }}"
+        l_double_upgrade_saved_tag: "{{ openshift_image_tag }}"
+        l_double_upgrade_saved_pkgv: "{{ openshift_pkg_version }}"
+    - set_fact:
+        # We already ran openshift_version for the second of two upgrades;
+        # here we need to set some variables to enable the first upgrade.
+        # openshift_version, openshift_image_tag, and openshift_pkg_version
+        # will be modified by openshift_version; we want to ensure these
+        # are initially set to first versions to ensure no accidental usage of
+        # second versions (eg, 3.8 and 3.9 respectively) are used.
+        l_double_upgrade_cp_reset_version: True
+        openshift_version: "{{ l_double_upgrade_first_version }}"
+        openshift_release: "{{ l_double_upgrade_first_release }}"
+        openshift_upgrade_target: '3.8'
+        openshift_upgrade_min: '3.7'
+
+# Now that we have force-set a different version, we need to update a few things
+# to ensure we have settings that actually match what's in repos/registries.
+- import_playbook: ../../../../init/version.yml

+ 4 - 0
playbooks/common/openshift-cluster/upgrades/upgrade_control_plane.yml

@@ -81,6 +81,10 @@
     - openshift_cloudprovider_kind in 'vsphere'
     - openshift_version | version_compare('3.9', '>=')
 
+  - name: Setup and enable bootstrapping options
+    include_tasks: ../../../openshift-master/private/tasks/enable_bootstrap.yml
+    when: openshift_master_bootstrap_enabled | default(false) | bool
+
   # Run the upgrade hook prior to restarting services/system if defined:
   - debug: msg="Running master upgrade hook {{ openshift_master_upgrade_hook }}"
     when: openshift_master_upgrade_hook is defined

+ 0 - 7
playbooks/common/openshift-cluster/upgrades/v3_10/validator.yml

@@ -1,7 +0,0 @@
----
-- name: Verify 3.8 specific upgrade checks
-  hosts: oo_first_master
-  roles:
-  - { role: lib_openshift }
-  tasks:
-  - debug: msg="noop"

+ 0 - 2
playbooks/common/openshift-cluster/upgrades/v3_8/upgrade.yml

@@ -25,8 +25,6 @@
     l_upgrade_excluder_hosts: "oo_nodes_to_config:oo_masters_to_config"
     openshift_protect_installed_version: False
 
-- import_playbook: validator.yml
-
 - name: Flag pre-upgrade checks complete for hosts without errors
   hosts: oo_masters_to_config:oo_nodes_to_upgrade:oo_etcd_to_config
   tasks:

+ 0 - 2
playbooks/common/openshift-cluster/upgrades/v3_8/upgrade_control_plane.yml

@@ -38,8 +38,6 @@
     l_upgrade_excluder_hosts: "oo_masters_to_config"
     openshift_protect_installed_version: False
 
-- import_playbook: validator.yml
-
 - name: Flag pre-upgrade checks complete for hosts without errors
   hosts: oo_masters_to_config:oo_etcd_to_config
   tasks:

+ 0 - 7
playbooks/common/openshift-cluster/upgrades/v3_8/validator.yml

@@ -1,7 +0,0 @@
----
-- name: Verify 3.8 specific upgrade checks
-  hosts: oo_first_master
-  roles:
-  - { role: lib_openshift }
-  tasks:
-  - debug: msg="noop"

+ 38 - 37
playbooks/common/openshift-cluster/upgrades/v3_9/upgrade_control_plane.yml

@@ -17,32 +17,32 @@
     l_init_fact_hosts: "oo_masters_to_config:oo_etcd_to_config:oo_lb_to_config"
     l_base_packages_hosts: "oo_masters_to_config:oo_etcd_to_config:oo_lb_to_config"
 
-## Check to see if they're running 3.7 and if so upgrade them to 3.8 on control plan
-## If they've specified pkg_version or image_tag preserve that for later use
-- name: Configure the upgrade target for the common upgrade tasks 3.8
+- name: Configure the initial upgrade target for the common upgrade tasks
   hosts: oo_masters_to_config:oo_etcd_to_config:oo_lb_to_config
   tasks:
   - set_fact:
-      openshift_upgrade_target: '3.8'
+      # We use 3.9 here so when we run openshift_version we can get
+      # correct values for 3.9, 3.8 we will hard-code the values in
+      # ../pre/version_override.yml, if necessary.
+      openshift_upgrade_target: '3.9'
       openshift_upgrade_min: '3.7'
-      openshift_release: '3.8'
-      _requested_pkg_version: "{{ openshift_pkg_version if openshift_pkg_version is defined else omit }}"
-      openshift_pkg_version: ''
-      _requested_image_tag: "{{ openshift_image_tag if openshift_image_tag is defined else omit }}"
+
+## Check to see if we need to double upgrade (3.7 -> 3.8 -> 3.9)
+- name: Configure variables for double upgrade
+  hosts: oo_masters_to_config:oo_etcd_to_config
+  tasks:
+  - set_fact:
       l_double_upgrade_cp: True
+      l_version_override_hosts: "oo_masters_to_config:oo_etcd_to_config"
+      l_double_upgrade_first_version: "3.8"
+      l_double_upgrade_first_release: "3.8"
     when: hostvars[groups.oo_first_master.0].openshift_currently_installed_version | version_compare('3.8','<')
 
-  - name: set l_force_image_tag_to_version = True
-    set_fact:
-      # Need to set this during 3.8 upgrade to ensure image_tag is set correctly
-      # to match 3.8 version
-      l_force_image_tag_to_version: True
-    when: _requested_image_tag is defined
-
 - import_playbook: ../pre/config.yml
   # These vars a meant to exclude oo_nodes from plays that would otherwise include
   # them by default.
   vars:
+    l_version_override_hosts: "oo_masters_to_config:oo_etcd_to_config"
     l_openshift_version_set_hosts: "oo_etcd_to_config:oo_masters_to_config:!oo_first_master"
     l_openshift_version_check_hosts: "oo_masters_to_config:!oo_first_master"
     l_upgrade_repo_hosts: "oo_masters_to_config:oo_etcd_to_config:oo_lb_to_config"
@@ -52,46 +52,48 @@
     l_upgrade_docker_target_hosts: "oo_masters_to_config:oo_etcd_to_config"
     l_upgrade_excluder_hosts: "oo_masters_to_config"
     openshift_protect_installed_version: False
-  when: hostvars[groups.oo_first_master.0].openshift_currently_installed_version | version_compare('3.8','<')
+  when: l_double_upgrade_cp | default(False)
 
 - name: Flag pre-upgrade checks complete for hosts without errors 3.8
   hosts: oo_masters_to_config:oo_etcd_to_config
   tasks:
   - set_fact:
       pre_upgrade_complete: True
-    when: hostvars[groups.oo_first_master.0].openshift_currently_installed_version | version_compare('3.8','<')
+    when: l_double_upgrade_cp | default(False)
 
 # Pre-upgrade completed
 
 - name: Intermediate 3.8 Upgrade
   import_playbook: ../upgrade_control_plane.yml
-  when: hostvars[groups.oo_first_master.0].openshift_currently_installed_version | version_compare('3.8','<')
+  when: l_double_upgrade_cp | default(False)
+
+- name: Restore 3.9 version variables
+  hosts: oo_masters_to_config:oo_etcd_to_config
+  tasks:
+  - set_fact:
+      # all:!all == 0 hosts
+      l_version_override_hosts: "all:!all"
+      openshift_version: "{{ l_double_upgrade_saved_version }}"
+      openshift_release: "{{ l_double_upgrade_saved_release }}"
+      openshift_image_tag: "{{ l_double_upgrade_saved_tag }}"
+      openshift_pkg_version: "{{ l_double_upgrade_saved_pkgv }}"
+    when: l_double_upgrade_cp | default(False)
 
 ## 3.8 upgrade complete we should now be able to upgrade to 3.9
+- name: Clear some values now that we're done with double upgrades.
+  hosts: oo_masters_to_config:oo_etcd_to_config
+  tasks:
+  - set_fact:
+      l_double_upgrade_cp: False
+      l_double_upgrade_cp_reset_version: False
 
-- name: Configure the upgrade target for the common upgrade tasks 3.9
+# We should be on 3.8 at this point, need to set upgrade_target to 3.9
+- name: Configure the upgrade target for second upgrade
   hosts: oo_masters_to_config:oo_etcd_to_config:oo_lb_to_config
   tasks:
-  - meta: clear_facts
   - set_fact:
       openshift_upgrade_target: '3.9'
       openshift_upgrade_min: '3.8'
-      openshift_release: '3.9'
-      openshift_pkg_version: "{{ _requested_pkg_version if _requested_pkg_version is defined else '' }}"
-  # Set the user's specified image_tag for 3.9 upgrade if it was provided.
-  - set_fact:
-      openshift_image_tag: "{{ _requested_image_tag }}"
-      l_force_image_tag_to_version: False
-    when: _requested_image_tag is defined
-  # If the user didn't specify an image_tag, we need to force update image_tag
-  # because it will have already been set during 3.8.  If we aren't running
-  # a double upgrade, then we can preserve image_tag because it will still
-  # be the user provided value.
-  - set_fact:
-      l_force_image_tag_to_version: True
-    when:
-    - l_double_upgrade_cp is defined and l_double_upgrade_cp
-    - _requested_image_tag is not defined
 
 - import_playbook: ../pre/config.yml
   # These vars a meant to exclude oo_nodes from plays that would otherwise include
@@ -106,7 +108,6 @@
     l_upgrade_docker_target_hosts: "oo_masters_to_config:oo_etcd_to_config"
     l_upgrade_excluder_hosts: "oo_masters_to_config"
     openshift_protect_installed_version: False
-    openshift_version_reinit: True
 
 - name: Flag pre-upgrade checks complete for hosts without errors
   hosts: oo_masters_to_config:oo_etcd_to_config

+ 0 - 7
playbooks/common/openshift-cluster/upgrades/v3_9/validator.yml

@@ -1,7 +0,0 @@
----
-- name: Verify 3.8 specific upgrade checks
-  hosts: oo_first_master
-  roles:
-  - { role: lib_openshift }
-  tasks:
-  - debug: msg="noop"

+ 3 - 1
playbooks/container-runtime/config.yml

@@ -1,6 +1,8 @@
 ---
 - import_playbook: ../init/main.yml
   vars:
-    skip_verison: True
+    skip_version: True
+    l_openshift_version_set_hosts: "all:!all"
+    l_openshift_version_check_hosts: "all:!all"
 
 - import_playbook: private/config.yml

+ 3 - 1
playbooks/container-runtime/setup_storage.yml

@@ -1,6 +1,8 @@
 ---
 - import_playbook: ../init/main.yml
   vars:
-    skip_verison: True
+    skip_version: True
+    l_openshift_version_set_hosts: "all:!all"
+    l_openshift_version_check_hosts: "all:!all"
 
 - import_playbook: private/setup_storage.yml

+ 2 - 2
playbooks/init/main.yml

@@ -1,5 +1,5 @@
 ---
-# skip_verison and l_install_base_packages are passed in via prerequistes.yml.
+# skip_version and l_install_base_packages are passed in via prerequistes.yml.
 # skip_sanity_checks is passed in via openshift-node/private/image_prep.yml
 
 - name: Initialization Checkpoint Start
@@ -27,7 +27,7 @@
 - import_playbook: cluster_facts.yml
 
 - import_playbook: version.yml
-  when: not (skip_verison | default(False))
+  when: not (skip_version | default(False))
 
 - import_playbook: sanity_checks.yml
   when: not (skip_sanity_checks | default(False))

+ 1 - 1
playbooks/init/version.yml

@@ -1,7 +1,7 @@
 ---
 # NOTE: requires openshift_facts be run
 - name: Determine openshift_version to configure on first master
-  hosts: oo_first_master
+  hosts: "{{ l_openshift_version_determine_hosts | default('oo_first_master') }}"
   tasks:
   - include_role:
       name: openshift_version

+ 6 - 0
playbooks/openshift-etcd/certificates.yml

@@ -1,5 +1,11 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    skip_version: True
+    l_openshift_version_set_hosts: "all:!all"
+    l_openshift_version_check_hosts: "all:!all"
+    l_init_fact_hosts: "oo_masters_to_config:oo_etcd_to_config:oo_lb_to_config"
+    l_sanity_check_hosts: "{{ groups['oo_etcd_to_config'] | union(groups['oo_masters_to_config']) }}"
 
 - import_playbook: private/ca.yml
 

+ 6 - 0
playbooks/openshift-etcd/config.yml

@@ -1,4 +1,10 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    skip_version: True
+    l_openshift_version_set_hosts: "all:!all"
+    l_openshift_version_check_hosts: "all:!all"
+    l_init_fact_hosts: "oo_masters_to_config:oo_etcd_to_config:oo_lb_to_config"
+    l_sanity_check_hosts: "{{ groups['oo_etcd_to_config'] | union(groups['oo_masters_to_config']) }}"
 
 - import_playbook: private/config.yml

+ 6 - 0
playbooks/openshift-etcd/embedded2external.yml

@@ -1,4 +1,10 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    skip_version: True
+    l_openshift_version_set_hosts: "all:!all"
+    l_openshift_version_check_hosts: "all:!all"
+    l_init_fact_hosts: "oo_masters_to_config:oo_etcd_to_config:oo_lb_to_config"
+    l_sanity_check_hosts: "{{ groups['oo_etcd_to_config'] | union(groups['oo_masters_to_config']) }}"
 
 - import_playbook: private/embedded2external.yml

+ 6 - 0
playbooks/openshift-etcd/migrate.yml

@@ -1,4 +1,10 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    skip_version: True
+    l_openshift_version_set_hosts: "all:!all"
+    l_openshift_version_check_hosts: "all:!all"
+    l_init_fact_hosts: "oo_masters_to_config:oo_etcd_to_config:oo_lb_to_config"
+    l_sanity_check_hosts: "{{ groups['oo_etcd_to_config'] | union(groups['oo_masters_to_config']) }}"
 
 - import_playbook: private/migrate.yml

+ 6 - 0
playbooks/openshift-etcd/redeploy-ca.yml

@@ -1,4 +1,10 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    skip_version: True
+    l_openshift_version_set_hosts: "all:!all"
+    l_openshift_version_check_hosts: "all:!all"
+    l_init_fact_hosts: "oo_masters_to_config:oo_etcd_to_config:oo_lb_to_config"
+    l_sanity_check_hosts: "{{ groups['oo_etcd_to_config'] | union(groups['oo_masters_to_config']) }}"
 
 - import_playbook: private/redeploy-ca.yml

+ 6 - 0
playbooks/openshift-etcd/redeploy-certificates.yml

@@ -1,5 +1,11 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    skip_version: True
+    l_openshift_version_set_hosts: "all:!all"
+    l_openshift_version_check_hosts: "all:!all"
+    l_init_fact_hosts: "oo_masters_to_config:oo_etcd_to_config:oo_lb_to_config"
+    l_sanity_check_hosts: "{{ groups['oo_etcd_to_config'] | union(groups['oo_masters_to_config']) }}"
 
 - import_playbook: private/redeploy-certificates.yml
 

+ 6 - 0
playbooks/openshift-etcd/restart.yml

@@ -1,4 +1,10 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    skip_version: True
+    l_init_fact_hosts: "oo_masters_to_config:oo_etcd_to_config:oo_lb_to_config"
+    l_openshift_version_set_hosts: "all:!all"
+    l_openshift_version_check_hosts: "all:!all"
+    l_sanity_check_hosts: "{{ groups['oo_etcd_to_config'] | union(groups['oo_masters_to_config']) }}"
 
 - import_playbook: private/restart.yml

+ 4 - 1
playbooks/openshift-etcd/scaleup.yml

@@ -43,8 +43,11 @@
 # prerequisites, we can just init facts as normal.
 - import_playbook: ../init/main.yml
   vars:
-    skip_verison: True
+    skip_version: True
     l_init_fact_hosts: "oo_masters_to_config:oo_etcd_to_config:oo_lb_to_config:oo_new_etcd_to_config"
+    l_sanity_check_hosts: "{{ groups['oo_new_etcd_to_config'] | union(groups['oo_masters_to_config']) | union(groups['oo_etcd_to_config']) }}"
+    l_openshift_version_set_hosts: "all:!all"
+    l_openshift_version_check_hosts: "all:!all"
   when:
   - inventory_hostname in groups['oo_masters']
   - inventory_hostname in groups['oo_nodes_to_config']

+ 3 - 1
playbooks/openshift-etcd/upgrade.yml

@@ -1,7 +1,9 @@
 ---
 - import_playbook: ../init/main.yml
   vars:
-    skip_verison: True
+    skip_version: True
+    l_openshift_version_set_hosts: "all:!all"
+    l_openshift_version_check_hosts: "all:!all"
     l_init_fact_hosts: "oo_masters_to_config:oo_etcd_to_config:oo_lb_to_config"
     l_sanity_check_hosts: "{{ groups['oo_etcd_to_config'] | union(groups['oo_masters_to_config']) }}"
 

+ 5 - 0
playbooks/openshift-glusterfs/config.yml

@@ -1,4 +1,9 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    l_init_fact_hosts: "oo_masters_to_config:oo_glusterfs_to_config"
+    l_openshift_version_set_hosts: "oo_masters_to_config:!oo_first_master"
+    l_openshift_version_check_hosts: "all:!all"
+    l_sanity_check_hosts: "{{ groups['oo_masters_to_config'] | union(groups['oo_glusterfs_to_config']) }}"
 
 - import_playbook: private/config.yml

+ 4 - 5
playbooks/openshift-glusterfs/private/config.yml

@@ -19,11 +19,6 @@
       tasks_from: firewall.yml
     when:
     - openshift_storage_glusterfs_is_native | default(True) | bool
-  - import_role:
-      name: openshift_storage_glusterfs
-      tasks_from: kernel_modules.yml
-    when:
-    - openshift_storage_glusterfs_is_native | default(True) | bool
 
 - name: Open firewall ports for GlusterFS registry nodes
   hosts: glusterfs_registry
@@ -33,6 +28,10 @@
       tasks_from: firewall.yml
     when:
     - openshift_storage_glusterfs_registry_is_native | default(True) | bool
+
+- name: Load kernel modules for nodes
+  hosts: oo_nodes_to_config
+  tasks:
   - import_role:
       name: openshift_storage_glusterfs
       tasks_from: kernel_modules.yml

+ 8 - 0
playbooks/openshift-glusterfs/private/uninstall.yml

@@ -0,0 +1,8 @@
+---
+- name: Uninstall GlusterFS
+  hosts: oo_first_master
+  tasks:
+  - name: Run glusterfs uninstall role
+    include_role:
+      name: openshift_storage_glusterfs
+      tasks_from: uninstall.yml

+ 5 - 0
playbooks/openshift-glusterfs/registry.yml

@@ -1,4 +1,9 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    l_init_fact_hosts: "oo_masters_to_config:oo_glusterfs_to_config"
+    l_openshift_version_set_hosts: "oo_masters_to_config:!oo_first_master"
+    l_openshift_version_check_hosts: "all:!all"
+    l_sanity_check_hosts: "{{ groups['oo_masters_to_config'] | union(groups['oo_glusterfs_to_config']) }}"
 
 - import_playbook: private/registry.yml

+ 4 - 0
playbooks/openshift-glusterfs/uninstall.yml

@@ -0,0 +1,4 @@
+---
+- import_playbook: ../init/main.yml
+
+- import_playbook: private/uninstall.yml

+ 5 - 0
playbooks/openshift-grafana/config.yml

@@ -1,4 +1,9 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    l_init_fact_hosts: "oo_masters_to_config"
+    l_openshift_version_set_hosts: "oo_masters_to_config:!oo_first_master"
+    l_openshift_version_check_hosts: "all:!all"
+    l_sanity_check_hosts: "{{ groups['oo_masters_to_config'] }}"
 
 - import_playbook: private/config.yml

+ 5 - 0
playbooks/openshift-hosted/config.yml

@@ -1,4 +1,9 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    l_init_fact_hosts: "oo_masters_to_config"
+    l_openshift_version_set_hosts: "oo_masters_to_config:!oo_first_master"
+    l_openshift_version_check_hosts: "all:!all"
+    l_sanity_check_hosts: "{{ groups['oo_masters_to_config'] }}"
 
 - import_playbook: private/config.yml

+ 5 - 0
playbooks/openshift-hosted/deploy_registry.yml

@@ -1,4 +1,9 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    l_init_fact_hosts: "oo_masters_to_config"
+    l_openshift_version_set_hosts: "oo_masters_to_config:!oo_first_master"
+    l_openshift_version_check_hosts: "all:!all"
+    l_sanity_check_hosts: "{{ groups['oo_masters_to_config'] }}"
 
 - import_playbook: private/openshift_hosted_registry.yml

+ 5 - 0
playbooks/openshift-hosted/deploy_router.yml

@@ -1,4 +1,9 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    l_init_fact_hosts: "oo_masters_to_config"
+    l_openshift_version_set_hosts: "oo_masters_to_config:!oo_first_master"
+    l_openshift_version_check_hosts: "all:!all"
+    l_sanity_check_hosts: "{{ groups['oo_masters_to_config'] }}"
 
 - import_playbook: private/openshift_hosted_router.yml

+ 0 - 4
playbooks/openshift-hosted/private/openshift_hosted_registry.yml

@@ -2,12 +2,8 @@
 - name: Create Hosted Resources - registry
   hosts: oo_first_master
   tasks:
-  - set_fact:
-      openshift_hosted_registry_registryurl: "{{ hostvars[groups.oo_first_master.0].openshift.master.registry_url }}"
-    when: "'master' in hostvars[groups.oo_first_master.0].openshift and 'registry_url' in hostvars[groups.oo_first_master.0].openshift.master"
   - import_role:
       name: openshift_hosted
       tasks_from: registry.yml
     when:
     - openshift_hosted_manage_registry | default(True) | bool
-    - openshift_hosted_registry_registryurl is defined

+ 0 - 4
playbooks/openshift-hosted/private/openshift_hosted_router.yml

@@ -2,12 +2,8 @@
 - name: Create Hosted Resources - router
   hosts: oo_first_master
   tasks:
-  - set_fact:
-      openshift_hosted_router_registryurl: "{{ hostvars[groups.oo_first_master.0].openshift.master.registry_url }}"
-    when: "'master' in hostvars[groups.oo_first_master.0].openshift and 'registry_url' in hostvars[groups.oo_first_master.0].openshift.master"
   - import_role:
       name: openshift_hosted
       tasks_from: router.yml
     when:
     - openshift_hosted_manage_router | default(True) | bool
-    - openshift_hosted_router_registryurl is defined

+ 5 - 0
playbooks/openshift-hosted/redeploy-registry-certificates.yml

@@ -1,4 +1,9 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    l_init_fact_hosts: "oo_masters_to_config"
+    l_openshift_version_set_hosts: "oo_masters_to_config:!oo_first_master"
+    l_openshift_version_check_hosts: "all:!all"
+    l_sanity_check_hosts: "{{ groups['oo_masters_to_config'] }}"
 
 - import_playbook: private/redeploy-registry-certificates.yml

+ 5 - 0
playbooks/openshift-hosted/redeploy-router-certificates.yml

@@ -1,4 +1,9 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    l_init_fact_hosts: "oo_masters_to_config"
+    l_openshift_version_set_hosts: "oo_masters_to_config:!oo_first_master"
+    l_openshift_version_check_hosts: "all:!all"
+    l_sanity_check_hosts: "{{ groups['oo_masters_to_config'] }}"
 
 - import_playbook: private/redeploy-router-certificates.yml

+ 5 - 0
playbooks/openshift-loadbalancer/config.yml

@@ -1,4 +1,9 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    l_init_fact_hosts: "oo_masters_to_config:oo_lb_to_config"
+    l_openshift_version_set_hosts: "oo_masters_to_config:!oo_first_master"
+    l_openshift_version_check_hosts: "all:!all"
+    l_sanity_check_hosts: "{{ groups['oo_masters_to_config'] | union(groups['oo_lb_to_config']) }}"
 
 - import_playbook: private/config.yml

+ 5 - 0
playbooks/openshift-logging/config.yml

@@ -5,5 +5,10 @@
 # currently supported method.
 #
 - import_playbook: ../init/main.yml
+  vars:
+    l_init_fact_hosts: "oo_masters_to_config"
+    l_openshift_version_set_hosts: "oo_masters_to_config:!oo_first_master"
+    l_openshift_version_check_hosts: "all:!all"
+    l_sanity_check_hosts: "{{ groups['oo_masters_to_config'] }}"
 
 - import_playbook: private/config.yml

+ 5 - 0
playbooks/openshift-management/config.yml

@@ -1,4 +1,9 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    l_init_fact_hosts: "oo_masters_to_config"
+    l_openshift_version_set_hosts: "oo_masters_to_config:!oo_first_master"
+    l_openshift_version_check_hosts: "all:!all"
+    l_sanity_check_hosts: "{{ groups['oo_masters_to_config'] }}"
 
 - import_playbook: private/config.yml

+ 4 - 0
playbooks/openshift-master/enable_bootstrap.yml

@@ -0,0 +1,4 @@
+---
+- import_playbook: ../init/main.yml
+
+- import_playbook: private/enable_bootstrap.yml

+ 3 - 5
playbooks/openshift-master/private/config.yml

@@ -30,7 +30,7 @@
   # masters, or absent if such is the case.
   - name: Detect if this host is a new master in a scale up
     set_fact:
-      g_openshift_master_is_scaleup: "{{ openshift.common.hostname in ( groups['new_masters'] | default([]) ) }}"
+      g_openshift_master_is_scaleup: "{{ inventory_hostname in ( groups['new_masters'] | default([]) ) }}"
 
   - name: Scaleup Detection
     debug:
@@ -207,11 +207,9 @@
       tasks_from: master
     when: openshift_use_kuryr | default(false) | bool
 
-  - name: Setup the node group config maps
-    import_role:
-      name: openshift_node_group
+  - name: setup bootstrap settings
+    include_tasks: tasks/enable_bootstrap.yml
     when: openshift_master_bootstrap_enabled | default(false) | bool
-    run_once: True
 
   post_tasks:
   - name: Create group for deployment type

+ 7 - 0
playbooks/openshift-master/private/enable_bootstrap.yml

@@ -0,0 +1,7 @@
+---
+- name: Enable bootstrapping for masters
+  hosts: oo_masters_to_config
+  gather_facts: no
+  tasks:
+  - name: include bootstrapping tasks
+    include_tasks: tasks/enable_bootstrap.yml

+ 20 - 0
playbooks/openshift-master/private/tasks/enable_bootstrap.yml

@@ -0,0 +1,20 @@
+---
+- name: Setup the master bootstrap settings
+  import_role:
+    name: openshift_master
+    tasks_from: bootstrap_settings.yml
+
+- name: Setup the bootstrap kubeconfig
+  import_role:
+    name: openshift_master
+    tasks_from: bootstrap.yml
+
+- name: Setup the node group config maps
+  import_role:
+    name: openshift_node_group
+  run_once: True
+
+- name: Setup the node bootstrap auto approver
+  import_role:
+    name: openshift_bootstrap_autoapprover
+  run_once: True

+ 3 - 19
playbooks/openshift-master/private/tasks/restart_hosts.yml

@@ -7,26 +7,10 @@
   ignore_errors: true
   become: yes
 
-# WARNING: This process is riddled with weird behavior.
-
-# Workaround for https://github.com/ansible/ansible/issues/21269
-- set_fact:
-    wait_for_host: "{{ ansible_host }}"
-
-# Ansible's blog documents this *without* the port, which appears to now
-# just wait until the timeout value and then proceed without checking anything.
-# port is now required.
-#
-# However neither ansible_ssh_port or ansible_port are reliably defined, likely
-# only if overridden. Assume a default of 22.
 - name: Wait for master to restart
-  local_action:
-    module: wait_for
-      host="{{ wait_for_host }}"
-      state=started
-      delay=10
-      timeout=600
-      port="{{ ansible_port | default(ansible_ssh_port | default(22,boolean=True),boolean=True) }}"
+  wait_for_connection:
+    delay: 10
+    timeout: 600
 
 # Now that ssh is back up we can wait for API on the remote system,
 # avoiding some potential connection issues from local system:

+ 5 - 1
playbooks/openshift-master/private/validate_restart.yml

@@ -33,6 +33,7 @@
   - stat: path="{{ hostvars.localhost.mktemp.stdout }}"
     register: exists
     changed_when: false
+    when: "'stdout' in hostvars.localhost.mktemp"
 
 - name: Cleanup temp file on localhost
   hosts: localhost
@@ -41,6 +42,7 @@
   tasks:
   - file: path="{{ hostvars.localhost.mktemp.stdout }}" state=absent
     changed_when: false
+    when: "'stdout' in hostvars.localhost.mktemp"
 
 - name: Warn if restarting the system where ansible is running
   hosts: oo_masters_to_config
@@ -54,7 +56,9 @@
         must be verified manually. To only restart services, set
         openshift_master_rolling_restart_mode=services in host
         inventory and relaunch the playbook.
-    when: exists.stat.exists and openshift.common.rolling_restart_mode == 'system'
+    when:
+    - "'stat' in exists"
+    - exists.stat.exists and openshift.common.rolling_restart_mode == 'system'
   - set_fact:
       current_host: "{{ exists.stat.exists }}"
     when: openshift.common.rolling_restart_mode == 'system'

+ 6 - 0
playbooks/openshift-metrics/config.yml

@@ -1,4 +1,10 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    l_init_fact_hosts: "oo_masters_to_config"
+    l_openshift_version_set_hosts: "oo_masters_to_config:!oo_first_master"
+    l_openshift_version_check_hosts: "all:!all"
+    l_sanity_check_hosts: "{{ groups['oo_masters_to_config'] }}"
+
 
 - import_playbook: private/config.yml

+ 6 - 0
playbooks/openshift-nfs/config.yml

@@ -1,4 +1,10 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    l_init_fact_hosts: "oo_masters_to_config:oo_nfs_to_config"
+    l_openshift_version_set_hosts: "oo_masters_to_config:!oo_first_master"
+    l_openshift_version_check_hosts: "all:!all"
+    l_sanity_check_hosts: "{{ groups['oo_masters_to_config'] | union(groups['oo_nfs_to_config']) }}"
+
 
 - import_playbook: private/config.yml

+ 7 - 0
playbooks/openshift-node/private/image_prep.yml

@@ -6,6 +6,13 @@
     skip_sanity_checks: True
     skip_validate_hostnames: True
 
+- name: determine version
+  import_playbook: ../../init/version.yml
+  vars:
+    l_openshift_version_determine_hosts: "oo_nodes_to_config"
+    l_openshift_version_set_hosts: "all:!all"
+    l_openshift_version_check_hosts: "all:!all"
+
 - name: run node config setup
   import_playbook: setup.yml
 

+ 6 - 0
playbooks/openshift-prometheus/config.yml

@@ -1,4 +1,10 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    l_init_fact_hosts: "oo_masters_to_config"
+    l_openshift_version_set_hosts: "oo_masters_to_config:!oo_first_master"
+    l_openshift_version_check_hosts: "all:!all"
+    l_sanity_check_hosts: "{{ groups['oo_masters_to_config'] }}"
+
 
 - import_playbook: private/config.yml

+ 6 - 0
playbooks/openshift-provisioners/config.yml

@@ -1,4 +1,10 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    l_init_fact_hosts: "oo_masters_to_config"
+    l_openshift_version_set_hosts: "oo_masters_to_config:!oo_first_master"
+    l_openshift_version_check_hosts: "all:!all"
+    l_sanity_check_hosts: "{{ groups['oo_masters_to_config'] }}"
+
 
 - import_playbook: private/config.yml

+ 6 - 0
playbooks/openshift-service-catalog/config.yml

@@ -1,4 +1,10 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    l_init_fact_hosts: "oo_masters_to_config"
+    l_openshift_version_set_hosts: "oo_masters_to_config:!oo_first_master"
+    l_openshift_version_check_hosts: "all:!all"
+    l_sanity_check_hosts: "{{ groups['oo_masters_to_config'] }}"
+
 
 - import_playbook: private/config.yml

+ 5 - 0
playbooks/openshift-web-console/config.yml

@@ -1,4 +1,9 @@
 ---
 - import_playbook: ../init/main.yml
+  vars:
+    l_init_fact_hosts: "oo_masters_to_config"
+    l_openshift_version_set_hosts: "oo_masters_to_config:!oo_first_master"
+    l_openshift_version_check_hosts: "all:!all"
+    l_sanity_check_hosts: "{{ groups['oo_masters_to_config'] }}"
 
 - import_playbook: private/config.yml

+ 5 - 2
playbooks/openshift-web-console/private/config.yml

@@ -13,10 +13,13 @@
 
 - name: Web Console
   hosts: oo_first_master
-  roles:
-  - openshift_web_console
   vars:
     first_master: "{{ groups.oo_first_master[0] }}"
+  tasks:
+  - debug: msg="{{ openshift_version | version_compare('3.9', '>=') }}"
+  - import_role:
+      name: openshift_web_console
+    when: openshift_version | version_compare('3.9', '>=')
 
 - name: Web Console Install Checkpoint End
   hosts: all

+ 32 - 0
playbooks/openstack/advanced-configuration.md

@@ -273,6 +273,38 @@ openshift_openstack_cluster_node_labels:
     mylabel: myvalue
 ```
 
+`openshift_openstack_provision_user_commands` allows users to execute
+shell commands via cloud-init for all of the created Nova servers in
+the Heat stack, before they are available for SSH connections.
+Note that you should use custom ansible playbooks whenever
+possible, like this `provision_install_custom.yml` example playbook:
+```
+- import_playbook: openshift-ansible/playbooks/openstack/openshift-cluster/provision.yml
+
+- name: My custom actions
+  hosts: cluster_hosts
+  tasks:
+  - do whatever you want here
+
+- import_playbook: openshift-ansible/playbooks/openstack/openshift-cluster/install.yml
+```
+The playbook leverages a two existing provider interfaces: `provision.yml` and
+`install.yml`. For some cases, like SSH keys configuration and coordinated reboots of
+servers, the cloud-init runcmd directive may be a better choice though. User specified
+shell commands for cloud-init need to be either strings or lists, for example:
+```
+- openshift_openstack_provision_user_commands:
+  - set -vx
+  - systemctl stop sshd # fences off ansible playbooks as we want to reboot later
+  - ['echo', 'foo', '>', '/tmp/foo']
+  - [ ls, /tmp/foo, '||', true ]
+  - reboot # unfences ansible playbooks to continue after reboot
+```
+
+**Note** To protect Nova servers from recreating when the user-data changes via
+`openshift_openstack_provision_user_commands`, the
+`user_data_update_policy` parameter configured to `IGNORE` for Heat resources.
+
 The `openshift_openstack_nodes_to_remove` allows you to specify the numerical indexes
 of App nodes that should be removed; for example, ['0', '2'],
 

+ 1 - 1
playbooks/openstack/inventory.py

@@ -89,7 +89,7 @@ def build_inventory():
 
     for server in cluster_hosts:
         if 'group' in server.metadata:
-            group = server.metadata.group
+            group = server.metadata.get('group')
             if group not in inventory:
                 inventory[group] = {'hosts': []}
             inventory[group]['hosts'].append(server.name)

+ 1 - 2
playbooks/openstack/openshift-cluster/install.yml

@@ -8,8 +8,7 @@
 # values here. We do it in the OSEv3 group vars. Do we need to add
 # some logic here?
 
-- name: run the cluster deploy
-  import_playbook: ../../prerequisites.yml
+- import_playbook: ../../prerequisites.yml
 
 - name: run the cluster deploy
   import_playbook: ../../deploy_cluster.yml

+ 10 - 4
playbooks/openstack/openshift-cluster/provision.yml

@@ -26,9 +26,6 @@
   - name: Gather facts for the new nodes
     setup:
 
-- import_playbook: ../../init/basic_facts.yml
-- import_playbook: ../../init/cluster_facts.yml
-
 
 # TODO(shadower): consider splitting this up so people can stop here
 # and configure their DNS if they have to.
@@ -43,7 +40,10 @@
     - openshift_openstack_external_nsupdate_keys is defined
     - openshift_openstack_external_nsupdate_keys.private is defined or openshift_openstack_external_nsupdate_keys.public is defined
 
-- name: Prepare the Nodes in the cluster for installation
+
+- import_playbook: ../../init/basic_facts.yml
+
+- name: Optionally subscribe the RHEL nodes
   hosts: oo_all_hosts
   become: yes
   gather_facts: yes
@@ -63,6 +63,12 @@
     - ansible_distribution == "RedHat"
     - rh_subscribed is defined
 
+
+- name: Prepare the Nodes in the cluster for installation
+  hosts: oo_all_hosts
+  become: yes
+  gather_facts: yes
+  tasks:
   - name: Install dependencies
     import_role:
       name: openshift_openstack

+ 10 - 1
playbooks/openstack/sample-inventory/group_vars/all.yml

@@ -85,7 +85,12 @@ openshift_openstack_docker_volume_size: "15"
 ## WARNING: This will delete any data on the volume!
 #openshift_openstack_prepare_and_format_registry_volume: False
 
-openshift_openstack_subnet_prefix: "192.168.99"
+# The Classless Inter-Domain Routing (CIDR) for the OpenStack VM subnet.
+openshift_openstack_subnet_cidr: "192.168.99.0/24"
+# The starting IP address for the OpenStack subnet allocation pool.
+openshift_openstack_pool_start: "192.168.99.3"
+# The ending IP address for the OpenStack subnet allocation pool.
+openshift_openstack_pool_end: "192.168.99.254"
 
 ## Red Hat subscription:
 #rhsub_user: '<username>'
@@ -124,3 +129,7 @@ ansible_user: openshift
 #    region: primary
 #  infra:
 #    region: infra
+
+## cloud config
+openshift_openstack_disable_root: true
+openshift_openstack_user: openshift

+ 3 - 1
playbooks/prerequisites.yml

@@ -3,8 +3,10 @@
 
 - import_playbook: init/main.yml
   vars:
-    skip_verison: True
+    skip_version: True
     l_install_base_packages: True
+    l_openshift_version_set_hosts: "all:!all"
+    l_openshift_version_check_hosts: "all:!all"
 
 - import_playbook: init/validate_hostnames.yml
   when: not (skip_validate_hostnames | default(False))

+ 7 - 2
roles/container_runtime/defaults/main.yml

@@ -15,8 +15,7 @@ openshift_docker_service_name: "{{ 'container-engine' if (openshift_docker_use_s
 
 openshift_docker_hosted_registry_insecure: False  # bool
 
-openshift_docker_hosted_registry_network_default: "{{ openshift_portal_net | default(False) }}"
-openshift_docker_hosted_registry_network: "{{ openshift_docker_hosted_registry_network_default }}"
+openshift_docker_hosted_registry_network: "{{ openshift.common.portal_net }}"
 
 openshift_docker_additional_registries: []
 openshift_docker_blocked_registries: []
@@ -87,6 +86,8 @@ openshift_use_crio_only: False
 l_openshift_image_tag_default: "{{ openshift_release | default('latest') }}"
 l_openshift_image_tag: "{{ openshift_image_tag | default(l_openshift_image_tag_default) | string}}"
 
+l_required_docker_version: '1.12'
+
 # --------------------- #
 # systemcontainers_crio #
 # --------------------- #
@@ -101,6 +102,7 @@ l_additional_crio_registries: "{{ '\"{}\"'.format('\", \"'.join(l_crio_registrie
 #   options:
 #   - rw
 #   - mode=755
+#   type: bind
 container_runtime_crio_additional_mounts: []
 
 l_crio_additional_mounts: "{{ ',' + (container_runtime_crio_additional_mounts | lib_utils_oo_l_of_d_to_csv) if container_runtime_crio_additional_mounts != [] else '' }}"
@@ -142,3 +144,6 @@ l_docker_image_default: "{{ l_docker_image_prepend }}:{{ l_docker_image_tag }}"
 l_docker_image: "{{ openshift_docker_systemcontainer_image_override | default(l_docker_image_default) }}"
 
 l_is_node_system_container: "{{ (openshift_use_node_system_container | default(openshift_use_system_containers | default(false)) | bool) }}"
+
+l_crio_use_new_var_sock: "{{  (l_openshift_image_tag == 'latest') or (l_openshift_image_tag | version_compare('3.9', '>='))  | bool }}"
+l_crio_var_sock: "{{ l_crio_use_new_var_sock | ternary('/var/run/crio/crio.sock', '/var/run/crio.sock') }}"

+ 12 - 8
roles/container_runtime/tasks/docker_upgrade_check.yml

@@ -36,14 +36,16 @@
   failed_when: false
   changed_when: false
 
-- fail:
-    msg: This playbook requires access to Docker 1.12 or later
+- name: Required docker version not available (non-atomic)
+  fail:
+    msg: "This playbook requires access to Docker {{ l_required_docker_version }} or later"
   # Disable the 1.12 requirement if the user set a specific Docker version
   when:
     - not openshift_is_atomic | bool
     - docker_version is not defined
-    - docker_upgrade is not defined or docker_upgrade | bool == True
-    - (pkg_check.rc == 0 and (avail_docker_version.stdout == "" or avail_docker_version.stdout is version_compare('1.12','<')))
+    - docker_upgrade | bool
+    - pkg_check.rc == 0
+    - avail_docker_version.stdout == "" or avail_docker_version.stdout is version_compare(l_required_docker_version,'<')
 
 # Default l_docker_upgrade to False, we'll set to True if an upgrade is required:
 - set_fact:
@@ -54,7 +56,8 @@
     docker_version: "{{ avail_docker_version.stdout }}"
   when:
     - not openshift_is_atomic | bool
-    - pkg_check.rc == 0 and docker_version is not defined
+    - pkg_check.rc == 0
+    - docker_version is not defined
 
 - name: Flag for Docker upgrade if necessary
   set_fact:
@@ -74,8 +77,9 @@
     l_docker_version: "{{ g_atomic_docker_version_result.stdout | from_yaml }}"
   when: openshift_is_atomic | bool
 
-- fail:
-    msg: This playbook requires access to Docker 1.12 or later
+- name: Required docker version is unavailable (atomic)
+  fail:
+    msg: "This playbook requires access to Docker {{ l_required_docker_version }} or later"
   when:
     - openshift_is_atomic | bool
-    - l_docker_version.avail_version | default(l_docker_version.curr_version, true) is version_compare('1.12','<')
+    - l_docker_version.avail_version | default(l_docker_version.curr_version, true) is version_compare(l_required_docker_version,'<')

+ 3 - 3
roles/container_runtime/tasks/package_docker.yml

@@ -111,12 +111,12 @@
     regexp: '^OPTIONS=.*$'
     line: "OPTIONS='\
       {% if ansible_selinux.status | default(None) == 'enabled' and openshift_docker_selinux_enabled | default(true) | bool %} --selinux-enabled {% endif %} \
-      {% if openshift_docker_log_driver | bool %} --log-driver {{ openshift_docker_log_driver }}{% endif %} \
+      {% if openshift_docker_log_driver %} --log-driver {{ openshift_docker_log_driver }}{% endif %} \
       {% if l2_docker_log_options != [] %} {{ l2_docker_log_options |  lib_utils_oo_split() | lib_utils_oo_prepend_strings_in_list('--log-opt ') | join(' ')}}{% endif %} \
-      {% if openshift_docker_hosted_registry_insecure and (openshift_docker_hosted_registry_network | bool) %} --insecure-registry={{ openshift_docker_hosted_registry_network }} {% endif %} \
+      {% if (openshift_docker_hosted_registry_insecure | bool) and openshift_docker_hosted_registry_network %} --insecure-registry={{ openshift_docker_hosted_registry_network }} {% endif %} \
       {% if docker_options is defined %} {{ docker_options }}{% endif %} \
       {% if openshift_docker_options %} {{ openshift_docker_options }}{% endif %} \
-      {% if openshift_docker_disable_push_dockerhub %} --confirm-def-push={{ openshift_docker_disable_push_dockerhub | bool }}{% endif %} \
+      {% if openshift_docker_disable_push_dockerhub | bool %} --confirm-def-push={{ openshift_docker_disable_push_dockerhub | bool }}{% endif %} \
       --signature-verification={{ openshift_docker_signature_verification | bool }}'"
   when: docker_check.stat.isreg is defined and docker_check.stat.isreg
   notify:

+ 1 - 1
roles/container_runtime/templates/crio.conf.j2

@@ -27,7 +27,7 @@ storage_option = [
 [crio.api]
 
 # listen is the path to the AF_LOCAL socket on which crio will listen.
-listen = "/var/run/crio/crio.sock"
+listen = "{{ l_crio_var_sock }}"
 
 # stream_address is the IP address on which the stream server will listen
 stream_address = ""

+ 1 - 1
roles/etcd/defaults/main.yaml

@@ -78,7 +78,7 @@ etcd_listen_client_urls: "{{ etcd_url_scheme }}://{{ etcd_ip }}:{{ etcd_client_p
 
 # required role variable
 #etcd_peer: 127.0.0.1
-etcdctlv2: "etcdctl --cert-file {{ etcd_peer_cert_file }} --key-file {{ etcd_peer_key_file }} --ca-file {{ etcd_peer_ca_file }} -C https://{{ etcd_peer }}:{{ etcd_client_port }}"
+etcdctlv2: "{{ r_etcd_common_etcdctl_command }} --cert-file {{ etcd_peer_cert_file }} --key-file {{ etcd_peer_key_file }} --ca-file {{ etcd_peer_ca_file }} -C https://{{ etcd_peer }}:{{ etcd_client_port }}"
 
 etcd_service: "{{ 'etcd_container' if r_etcd_common_etcd_runtime == 'docker' else 'etcd' }}"
 # Location of the service file is fixed and not meant to be changed

+ 4 - 0
roles/kuryr/tasks/master.yaml

@@ -1,6 +1,7 @@
 ---
 - name: Perform OpenShift ServiceAccount config
   include_tasks: serviceaccount.yaml
+  run_once: true
 
 - name: Create kuryr manifests tempdir
   command: mktemp -d
@@ -32,6 +33,7 @@
     namespace: "{{ kuryr_namespace }}"
     files:
     - "{{ manifests_tmpdir.stdout }}/configmap.yaml"
+  run_once: true
 
 - name: Apply Controller Deployment manifest
   oc_obj:
@@ -41,6 +43,7 @@
     namespace: "{{ kuryr_namespace }}"
     files:
     - "{{ manifests_tmpdir.stdout }}/controller-deployment.yaml"
+  run_once: true
 
 - name: Apply kuryr-cni DaemonSet manifest
   oc_obj:
@@ -50,3 +53,4 @@
     namespace: "{{ kuryr_namespace }}"
     files:
     - "{{ manifests_tmpdir.stdout }}/cni-daemonset.yaml"
+  run_once: true

+ 65 - 0
roles/lib_openshift/action_plugins/conditional_set_fact.py

@@ -0,0 +1,65 @@
+"""
+Ansible action plugin to help with setting facts conditionally based on other facts.
+"""
+
+from ansible.plugins.action import ActionBase
+
+
+DOCUMENTATION = '''
+---
+action_plugin: conditional_set_fact
+
+short_description: This will set a fact if the value is defined
+
+description:
+    - "To avoid constant set_fact & when conditions for each var we can use this"
+
+author:
+    - Eric Wolinetz ewolinet@redhat.com
+'''
+
+
+EXAMPLES = '''
+- name: Conditionally set fact
+  conditional_set_fact:
+    fact1: not_defined_variable
+
+- name: Conditionally set fact
+  conditional_set_fact:
+    fact1: not_defined_variable
+    fact2: defined_variable
+
+- name: Conditionally set fact falling back on default
+  conditional_set_fact:
+    fact1: not_defined_var | defined_variable
+
+'''
+
+
+# pylint: disable=too-few-public-methods
+class ActionModule(ActionBase):
+    """Action plugin to execute deprecated var checks."""
+
+    def run(self, tmp=None, task_vars=None):
+        result = super(ActionModule, self).run(tmp, task_vars)
+        result['changed'] = False
+
+        facts = self._task.args.get('facts', [])
+        var_list = self._task.args.get('vars', [])
+
+        local_facts = dict()
+
+        for param in var_list:
+            other_vars = var_list[param].replace(" ", "")
+
+            for other_var in other_vars.split('|'):
+                if other_var in facts:
+                    local_facts[param] = facts[other_var]
+                    break
+
+        if local_facts:
+            result['changed'] = True
+
+        result['ansible_facts'] = local_facts
+
+        return result

+ 0 - 74
roles/lib_openshift/library/conditional_set_fact.py

@@ -1,74 +0,0 @@
-#!/usr/bin/python
-
-""" Ansible module to help with setting facts conditionally based on other facts """
-
-from ansible.module_utils.basic import AnsibleModule
-
-
-DOCUMENTATION = '''
----
-module: conditional_set_fact
-
-short_description: This will set a fact if the value is defined
-
-description:
-    - "To avoid constant set_fact & when conditions for each var we can use this"
-
-author:
-    - Eric Wolinetz ewolinet@redhat.com
-'''
-
-
-EXAMPLES = '''
-- name: Conditionally set fact
-  conditional_set_fact:
-    fact1: not_defined_variable
-
-- name: Conditionally set fact
-  conditional_set_fact:
-    fact1: not_defined_variable
-    fact2: defined_variable
-
-- name: Conditionally set fact falling back on default
-  conditional_set_fact:
-    fact1: not_defined_var | defined_variable
-
-'''
-
-
-def run_module():
-    """ The body of the module, we check if the variable name specified as the value
-        for the key is defined. If it is then we use that value as for the original key """
-
-    module = AnsibleModule(
-        argument_spec=dict(
-            facts=dict(type='dict', required=True),
-            vars=dict(required=False, type='dict', default=[])
-        ),
-        supports_check_mode=True
-    )
-
-    local_facts = dict()
-    is_changed = False
-
-    for param in module.params['vars']:
-        other_vars = module.params['vars'][param].replace(" ", "")
-
-        for other_var in other_vars.split('|'):
-            if other_var in module.params['facts']:
-                local_facts[param] = module.params['facts'][other_var]
-                if not is_changed:
-                    is_changed = True
-                break
-
-    return module.exit_json(changed=is_changed,  # noqa: F405
-                            ansible_facts=local_facts)
-
-
-def main():
-    """ main """
-    run_module()
-
-
-if __name__ == '__main__':
-    main()

+ 48 - 12
roles/lib_openshift/library/oc_adm_ca_server_cert.py

@@ -34,6 +34,7 @@
 from __future__ import print_function
 import atexit
 import copy
+import fcntl
 import json
 import os
 import re
@@ -221,14 +222,35 @@ class Yedit(object):  # pragma: no cover
 
         return True
 
+    # pylint: disable=too-many-return-statements,too-many-branches
     @staticmethod
-    def remove_entry(data, key, sep='.'):
+    def remove_entry(data, key, index=None, value=None, sep='.'):
         ''' remove data at location key '''
         if key == '' and isinstance(data, dict):
-            data.clear()
+            if value is not None:
+                data.pop(value)
+            elif index is not None:
+                raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
+            else:
+                data.clear()
+
             return True
+
         elif key == '' and isinstance(data, list):
-            del data[:]
+            ind = None
+            if value is not None:
+                try:
+                    ind = data.index(value)
+                except ValueError:
+                    return False
+            elif index is not None:
+                ind = index
+            else:
+                del data[:]
+
+            if ind is not None:
+                data.pop(ind)
+
             return True
 
         if not (key and Yedit.valid_key(key, sep)) and \
@@ -343,7 +365,9 @@ class Yedit(object):  # pragma: no cover
         tmp_filename = filename + '.yedit'
 
         with open(tmp_filename, 'w') as yfd:
+            fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             yfd.write(contents)
+            fcntl.flock(yfd, fcntl.LOCK_UN)
 
         os.rename(tmp_filename, filename)
 
@@ -362,10 +386,16 @@ class Yedit(object):  # pragma: no cover
             pass
 
         # Try to use RoundTripDumper if supported.
-        try:
-            Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
-        except AttributeError:
-            Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        if self.content_type == 'yaml':
+            try:
+                Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+            except AttributeError:
+                Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        elif self.content_type == 'json':
+            Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
+        else:
+            raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
+                                 'Please specify a content_type of yaml or json.')
 
         return (True, self.yaml_dict)
 
@@ -413,7 +443,7 @@ class Yedit(object):  # pragma: no cover
 
                 # Try to use RoundTripLoader if supported.
                 try:
-                    self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+                    self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
                 except AttributeError:
                     self.yaml_dict = yaml.safe_load(contents)
 
@@ -472,7 +502,7 @@ class Yedit(object):  # pragma: no cover
 
         return (False, self.yaml_dict)
 
-    def delete(self, path):
+    def delete(self, path, index=None, value=None):
         ''' remove path from a dict'''
         try:
             entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
@@ -482,7 +512,7 @@ class Yedit(object):  # pragma: no cover
         if entry is None:
             return (False, self.yaml_dict)
 
-        result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+        result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
         if not result:
             return (False, self.yaml_dict)
 
@@ -658,7 +688,12 @@ class Yedit(object):  # pragma: no cover
 
         curr_value = invalue
         if val_type == 'yaml':
-            curr_value = yaml.load(invalue)
+            try:
+                # AUDIT:maybe-no-member makes sense due to different yaml libraries
+                # pylint: disable=maybe-no-member
+                curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
+            except AttributeError:
+                curr_value = yaml.safe_load(invalue)
         elif val_type == 'json':
             curr_value = json.loads(invalue)
 
@@ -727,6 +762,7 @@ class Yedit(object):  # pragma: no cover
         '''perform the idempotent crud operations'''
         yamlfile = Yedit(filename=params['src'],
                          backup=params['backup'],
+                         content_type=params['content_type'],
                          separator=params['separator'])
 
         state = params['state']
@@ -757,7 +793,7 @@ class Yedit(object):  # pragma: no cover
             if params['update']:
                 rval = yamlfile.pop(params['key'], params['value'])
             else:
-                rval = yamlfile.delete(params['key'])
+                rval = yamlfile.delete(params['key'], params['index'], params['value'])
 
             if rval[0] and params['src']:
                 yamlfile.write()

+ 55 - 12
roles/lib_openshift/library/oc_adm_csr.py

@@ -34,6 +34,7 @@
 from __future__ import print_function
 import atexit
 import copy
+import fcntl
 import json
 import os
 import re
@@ -199,14 +200,35 @@ class Yedit(object):  # pragma: no cover
 
         return True
 
+    # pylint: disable=too-many-return-statements,too-many-branches
     @staticmethod
-    def remove_entry(data, key, sep='.'):
+    def remove_entry(data, key, index=None, value=None, sep='.'):
         ''' remove data at location key '''
         if key == '' and isinstance(data, dict):
-            data.clear()
+            if value is not None:
+                data.pop(value)
+            elif index is not None:
+                raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
+            else:
+                data.clear()
+
             return True
+
         elif key == '' and isinstance(data, list):
-            del data[:]
+            ind = None
+            if value is not None:
+                try:
+                    ind = data.index(value)
+                except ValueError:
+                    return False
+            elif index is not None:
+                ind = index
+            else:
+                del data[:]
+
+            if ind is not None:
+                data.pop(ind)
+
             return True
 
         if not (key and Yedit.valid_key(key, sep)) and \
@@ -321,7 +343,9 @@ class Yedit(object):  # pragma: no cover
         tmp_filename = filename + '.yedit'
 
         with open(tmp_filename, 'w') as yfd:
+            fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             yfd.write(contents)
+            fcntl.flock(yfd, fcntl.LOCK_UN)
 
         os.rename(tmp_filename, filename)
 
@@ -340,10 +364,16 @@ class Yedit(object):  # pragma: no cover
             pass
 
         # Try to use RoundTripDumper if supported.
-        try:
-            Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
-        except AttributeError:
-            Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        if self.content_type == 'yaml':
+            try:
+                Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+            except AttributeError:
+                Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        elif self.content_type == 'json':
+            Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
+        else:
+            raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
+                                 'Please specify a content_type of yaml or json.')
 
         return (True, self.yaml_dict)
 
@@ -391,7 +421,7 @@ class Yedit(object):  # pragma: no cover
 
                 # Try to use RoundTripLoader if supported.
                 try:
-                    self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+                    self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
                 except AttributeError:
                     self.yaml_dict = yaml.safe_load(contents)
 
@@ -450,7 +480,7 @@ class Yedit(object):  # pragma: no cover
 
         return (False, self.yaml_dict)
 
-    def delete(self, path):
+    def delete(self, path, index=None, value=None):
         ''' remove path from a dict'''
         try:
             entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
@@ -460,7 +490,7 @@ class Yedit(object):  # pragma: no cover
         if entry is None:
             return (False, self.yaml_dict)
 
-        result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+        result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
         if not result:
             return (False, self.yaml_dict)
 
@@ -636,7 +666,12 @@ class Yedit(object):  # pragma: no cover
 
         curr_value = invalue
         if val_type == 'yaml':
-            curr_value = yaml.load(invalue)
+            try:
+                # AUDIT:maybe-no-member makes sense due to different yaml libraries
+                # pylint: disable=maybe-no-member
+                curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
+            except AttributeError:
+                curr_value = yaml.safe_load(invalue)
         elif val_type == 'json':
             curr_value = json.loads(invalue)
 
@@ -705,6 +740,7 @@ class Yedit(object):  # pragma: no cover
         '''perform the idempotent crud operations'''
         yamlfile = Yedit(filename=params['src'],
                          backup=params['backup'],
+                         content_type=params['content_type'],
                          separator=params['separator'])
 
         state = params['state']
@@ -735,7 +771,7 @@ class Yedit(object):  # pragma: no cover
             if params['update']:
                 rval = yamlfile.pop(params['key'], params['value'])
             else:
-                rval = yamlfile.delete(params['key'])
+                rval = yamlfile.delete(params['key'], params['index'], params['value'])
 
             if rval[0] and params['src']:
                 yamlfile.write()
@@ -1639,6 +1675,7 @@ def main():
             timeout=dict(default=30, type='int'),
             approve_all=dict(default=False, type='bool'),
             service_account=dict(default='node-bootstrapper', type='str'),
+            fail_on_timeout=dict(default=False, type='bool'),
         ),
         supports_check_mode=True,
         mutually_exclusive=[['approve_all', 'nodes']],
@@ -1649,6 +1686,12 @@ def main():
 
     rval = OCcsr.run_ansible(module.params, module.check_mode)
 
+    # If we timed out then we weren't finished. Fail if user requested to fail.
+    if (module.params['timeout'] > 0 and
+            module.params['fail_on_timeout'] and
+            rval['timeout']):
+        return module.fail_json(msg='Timed out accepting certificate signing requests. Failing as requested.', **rval)
+
     if 'failed' in rval:
         return module.fail_json(**rval)
 

+ 48 - 12
roles/lib_openshift/library/oc_adm_manage_node.py

@@ -34,6 +34,7 @@
 from __future__ import print_function
 import atexit
 import copy
+import fcntl
 import json
 import os
 import re
@@ -207,14 +208,35 @@ class Yedit(object):  # pragma: no cover
 
         return True
 
+    # pylint: disable=too-many-return-statements,too-many-branches
     @staticmethod
-    def remove_entry(data, key, sep='.'):
+    def remove_entry(data, key, index=None, value=None, sep='.'):
         ''' remove data at location key '''
         if key == '' and isinstance(data, dict):
-            data.clear()
+            if value is not None:
+                data.pop(value)
+            elif index is not None:
+                raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
+            else:
+                data.clear()
+
             return True
+
         elif key == '' and isinstance(data, list):
-            del data[:]
+            ind = None
+            if value is not None:
+                try:
+                    ind = data.index(value)
+                except ValueError:
+                    return False
+            elif index is not None:
+                ind = index
+            else:
+                del data[:]
+
+            if ind is not None:
+                data.pop(ind)
+
             return True
 
         if not (key and Yedit.valid_key(key, sep)) and \
@@ -329,7 +351,9 @@ class Yedit(object):  # pragma: no cover
         tmp_filename = filename + '.yedit'
 
         with open(tmp_filename, 'w') as yfd:
+            fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             yfd.write(contents)
+            fcntl.flock(yfd, fcntl.LOCK_UN)
 
         os.rename(tmp_filename, filename)
 
@@ -348,10 +372,16 @@ class Yedit(object):  # pragma: no cover
             pass
 
         # Try to use RoundTripDumper if supported.
-        try:
-            Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
-        except AttributeError:
-            Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        if self.content_type == 'yaml':
+            try:
+                Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+            except AttributeError:
+                Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        elif self.content_type == 'json':
+            Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
+        else:
+            raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
+                                 'Please specify a content_type of yaml or json.')
 
         return (True, self.yaml_dict)
 
@@ -399,7 +429,7 @@ class Yedit(object):  # pragma: no cover
 
                 # Try to use RoundTripLoader if supported.
                 try:
-                    self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+                    self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
                 except AttributeError:
                     self.yaml_dict = yaml.safe_load(contents)
 
@@ -458,7 +488,7 @@ class Yedit(object):  # pragma: no cover
 
         return (False, self.yaml_dict)
 
-    def delete(self, path):
+    def delete(self, path, index=None, value=None):
         ''' remove path from a dict'''
         try:
             entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
@@ -468,7 +498,7 @@ class Yedit(object):  # pragma: no cover
         if entry is None:
             return (False, self.yaml_dict)
 
-        result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+        result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
         if not result:
             return (False, self.yaml_dict)
 
@@ -644,7 +674,12 @@ class Yedit(object):  # pragma: no cover
 
         curr_value = invalue
         if val_type == 'yaml':
-            curr_value = yaml.load(invalue)
+            try:
+                # AUDIT:maybe-no-member makes sense due to different yaml libraries
+                # pylint: disable=maybe-no-member
+                curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
+            except AttributeError:
+                curr_value = yaml.safe_load(invalue)
         elif val_type == 'json':
             curr_value = json.loads(invalue)
 
@@ -713,6 +748,7 @@ class Yedit(object):  # pragma: no cover
         '''perform the idempotent crud operations'''
         yamlfile = Yedit(filename=params['src'],
                          backup=params['backup'],
+                         content_type=params['content_type'],
                          separator=params['separator'])
 
         state = params['state']
@@ -743,7 +779,7 @@ class Yedit(object):  # pragma: no cover
             if params['update']:
                 rval = yamlfile.pop(params['key'], params['value'])
             else:
-                rval = yamlfile.delete(params['key'])
+                rval = yamlfile.delete(params['key'], params['index'], params['value'])
 
             if rval[0] and params['src']:
                 yamlfile.write()

+ 48 - 12
roles/lib_openshift/library/oc_adm_policy_group.py

@@ -34,6 +34,7 @@
 from __future__ import print_function
 import atexit
 import copy
+import fcntl
 import json
 import os
 import re
@@ -193,14 +194,35 @@ class Yedit(object):  # pragma: no cover
 
         return True
 
+    # pylint: disable=too-many-return-statements,too-many-branches
     @staticmethod
-    def remove_entry(data, key, sep='.'):
+    def remove_entry(data, key, index=None, value=None, sep='.'):
         ''' remove data at location key '''
         if key == '' and isinstance(data, dict):
-            data.clear()
+            if value is not None:
+                data.pop(value)
+            elif index is not None:
+                raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
+            else:
+                data.clear()
+
             return True
+
         elif key == '' and isinstance(data, list):
-            del data[:]
+            ind = None
+            if value is not None:
+                try:
+                    ind = data.index(value)
+                except ValueError:
+                    return False
+            elif index is not None:
+                ind = index
+            else:
+                del data[:]
+
+            if ind is not None:
+                data.pop(ind)
+
             return True
 
         if not (key and Yedit.valid_key(key, sep)) and \
@@ -315,7 +337,9 @@ class Yedit(object):  # pragma: no cover
         tmp_filename = filename + '.yedit'
 
         with open(tmp_filename, 'w') as yfd:
+            fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             yfd.write(contents)
+            fcntl.flock(yfd, fcntl.LOCK_UN)
 
         os.rename(tmp_filename, filename)
 
@@ -334,10 +358,16 @@ class Yedit(object):  # pragma: no cover
             pass
 
         # Try to use RoundTripDumper if supported.
-        try:
-            Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
-        except AttributeError:
-            Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        if self.content_type == 'yaml':
+            try:
+                Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+            except AttributeError:
+                Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        elif self.content_type == 'json':
+            Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
+        else:
+            raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
+                                 'Please specify a content_type of yaml or json.')
 
         return (True, self.yaml_dict)
 
@@ -385,7 +415,7 @@ class Yedit(object):  # pragma: no cover
 
                 # Try to use RoundTripLoader if supported.
                 try:
-                    self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+                    self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
                 except AttributeError:
                     self.yaml_dict = yaml.safe_load(contents)
 
@@ -444,7 +474,7 @@ class Yedit(object):  # pragma: no cover
 
         return (False, self.yaml_dict)
 
-    def delete(self, path):
+    def delete(self, path, index=None, value=None):
         ''' remove path from a dict'''
         try:
             entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
@@ -454,7 +484,7 @@ class Yedit(object):  # pragma: no cover
         if entry is None:
             return (False, self.yaml_dict)
 
-        result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+        result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
         if not result:
             return (False, self.yaml_dict)
 
@@ -630,7 +660,12 @@ class Yedit(object):  # pragma: no cover
 
         curr_value = invalue
         if val_type == 'yaml':
-            curr_value = yaml.load(invalue)
+            try:
+                # AUDIT:maybe-no-member makes sense due to different yaml libraries
+                # pylint: disable=maybe-no-member
+                curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
+            except AttributeError:
+                curr_value = yaml.safe_load(invalue)
         elif val_type == 'json':
             curr_value = json.loads(invalue)
 
@@ -699,6 +734,7 @@ class Yedit(object):  # pragma: no cover
         '''perform the idempotent crud operations'''
         yamlfile = Yedit(filename=params['src'],
                          backup=params['backup'],
+                         content_type=params['content_type'],
                          separator=params['separator'])
 
         state = params['state']
@@ -729,7 +765,7 @@ class Yedit(object):  # pragma: no cover
             if params['update']:
                 rval = yamlfile.pop(params['key'], params['value'])
             else:
-                rval = yamlfile.delete(params['key'])
+                rval = yamlfile.delete(params['key'], params['index'], params['value'])
 
             if rval[0] and params['src']:
                 yamlfile.write()

+ 48 - 12
roles/lib_openshift/library/oc_adm_policy_user.py

@@ -34,6 +34,7 @@
 from __future__ import print_function
 import atexit
 import copy
+import fcntl
 import json
 import os
 import re
@@ -207,14 +208,35 @@ class Yedit(object):  # pragma: no cover
 
         return True
 
+    # pylint: disable=too-many-return-statements,too-many-branches
     @staticmethod
-    def remove_entry(data, key, sep='.'):
+    def remove_entry(data, key, index=None, value=None, sep='.'):
         ''' remove data at location key '''
         if key == '' and isinstance(data, dict):
-            data.clear()
+            if value is not None:
+                data.pop(value)
+            elif index is not None:
+                raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
+            else:
+                data.clear()
+
             return True
+
         elif key == '' and isinstance(data, list):
-            del data[:]
+            ind = None
+            if value is not None:
+                try:
+                    ind = data.index(value)
+                except ValueError:
+                    return False
+            elif index is not None:
+                ind = index
+            else:
+                del data[:]
+
+            if ind is not None:
+                data.pop(ind)
+
             return True
 
         if not (key and Yedit.valid_key(key, sep)) and \
@@ -329,7 +351,9 @@ class Yedit(object):  # pragma: no cover
         tmp_filename = filename + '.yedit'
 
         with open(tmp_filename, 'w') as yfd:
+            fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             yfd.write(contents)
+            fcntl.flock(yfd, fcntl.LOCK_UN)
 
         os.rename(tmp_filename, filename)
 
@@ -348,10 +372,16 @@ class Yedit(object):  # pragma: no cover
             pass
 
         # Try to use RoundTripDumper if supported.
-        try:
-            Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
-        except AttributeError:
-            Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        if self.content_type == 'yaml':
+            try:
+                Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+            except AttributeError:
+                Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        elif self.content_type == 'json':
+            Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
+        else:
+            raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
+                                 'Please specify a content_type of yaml or json.')
 
         return (True, self.yaml_dict)
 
@@ -399,7 +429,7 @@ class Yedit(object):  # pragma: no cover
 
                 # Try to use RoundTripLoader if supported.
                 try:
-                    self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+                    self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
                 except AttributeError:
                     self.yaml_dict = yaml.safe_load(contents)
 
@@ -458,7 +488,7 @@ class Yedit(object):  # pragma: no cover
 
         return (False, self.yaml_dict)
 
-    def delete(self, path):
+    def delete(self, path, index=None, value=None):
         ''' remove path from a dict'''
         try:
             entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
@@ -468,7 +498,7 @@ class Yedit(object):  # pragma: no cover
         if entry is None:
             return (False, self.yaml_dict)
 
-        result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+        result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
         if not result:
             return (False, self.yaml_dict)
 
@@ -644,7 +674,12 @@ class Yedit(object):  # pragma: no cover
 
         curr_value = invalue
         if val_type == 'yaml':
-            curr_value = yaml.load(invalue)
+            try:
+                # AUDIT:maybe-no-member makes sense due to different yaml libraries
+                # pylint: disable=maybe-no-member
+                curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
+            except AttributeError:
+                curr_value = yaml.safe_load(invalue)
         elif val_type == 'json':
             curr_value = json.loads(invalue)
 
@@ -713,6 +748,7 @@ class Yedit(object):  # pragma: no cover
         '''perform the idempotent crud operations'''
         yamlfile = Yedit(filename=params['src'],
                          backup=params['backup'],
+                         content_type=params['content_type'],
                          separator=params['separator'])
 
         state = params['state']
@@ -743,7 +779,7 @@ class Yedit(object):  # pragma: no cover
             if params['update']:
                 rval = yamlfile.pop(params['key'], params['value'])
             else:
-                rval = yamlfile.delete(params['key'])
+                rval = yamlfile.delete(params['key'], params['index'], params['value'])
 
             if rval[0] and params['src']:
                 yamlfile.write()

+ 48 - 12
roles/lib_openshift/library/oc_adm_registry.py

@@ -34,6 +34,7 @@
 from __future__ import print_function
 import atexit
 import copy
+import fcntl
 import json
 import os
 import re
@@ -311,14 +312,35 @@ class Yedit(object):  # pragma: no cover
 
         return True
 
+    # pylint: disable=too-many-return-statements,too-many-branches
     @staticmethod
-    def remove_entry(data, key, sep='.'):
+    def remove_entry(data, key, index=None, value=None, sep='.'):
         ''' remove data at location key '''
         if key == '' and isinstance(data, dict):
-            data.clear()
+            if value is not None:
+                data.pop(value)
+            elif index is not None:
+                raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
+            else:
+                data.clear()
+
             return True
+
         elif key == '' and isinstance(data, list):
-            del data[:]
+            ind = None
+            if value is not None:
+                try:
+                    ind = data.index(value)
+                except ValueError:
+                    return False
+            elif index is not None:
+                ind = index
+            else:
+                del data[:]
+
+            if ind is not None:
+                data.pop(ind)
+
             return True
 
         if not (key and Yedit.valid_key(key, sep)) and \
@@ -433,7 +455,9 @@ class Yedit(object):  # pragma: no cover
         tmp_filename = filename + '.yedit'
 
         with open(tmp_filename, 'w') as yfd:
+            fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             yfd.write(contents)
+            fcntl.flock(yfd, fcntl.LOCK_UN)
 
         os.rename(tmp_filename, filename)
 
@@ -452,10 +476,16 @@ class Yedit(object):  # pragma: no cover
             pass
 
         # Try to use RoundTripDumper if supported.
-        try:
-            Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
-        except AttributeError:
-            Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        if self.content_type == 'yaml':
+            try:
+                Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+            except AttributeError:
+                Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        elif self.content_type == 'json':
+            Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
+        else:
+            raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
+                                 'Please specify a content_type of yaml or json.')
 
         return (True, self.yaml_dict)
 
@@ -503,7 +533,7 @@ class Yedit(object):  # pragma: no cover
 
                 # Try to use RoundTripLoader if supported.
                 try:
-                    self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+                    self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
                 except AttributeError:
                     self.yaml_dict = yaml.safe_load(contents)
 
@@ -562,7 +592,7 @@ class Yedit(object):  # pragma: no cover
 
         return (False, self.yaml_dict)
 
-    def delete(self, path):
+    def delete(self, path, index=None, value=None):
         ''' remove path from a dict'''
         try:
             entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
@@ -572,7 +602,7 @@ class Yedit(object):  # pragma: no cover
         if entry is None:
             return (False, self.yaml_dict)
 
-        result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+        result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
         if not result:
             return (False, self.yaml_dict)
 
@@ -748,7 +778,12 @@ class Yedit(object):  # pragma: no cover
 
         curr_value = invalue
         if val_type == 'yaml':
-            curr_value = yaml.load(invalue)
+            try:
+                # AUDIT:maybe-no-member makes sense due to different yaml libraries
+                # pylint: disable=maybe-no-member
+                curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
+            except AttributeError:
+                curr_value = yaml.safe_load(invalue)
         elif val_type == 'json':
             curr_value = json.loads(invalue)
 
@@ -817,6 +852,7 @@ class Yedit(object):  # pragma: no cover
         '''perform the idempotent crud operations'''
         yamlfile = Yedit(filename=params['src'],
                          backup=params['backup'],
+                         content_type=params['content_type'],
                          separator=params['separator'])
 
         state = params['state']
@@ -847,7 +883,7 @@ class Yedit(object):  # pragma: no cover
             if params['update']:
                 rval = yamlfile.pop(params['key'], params['value'])
             else:
-                rval = yamlfile.delete(params['key'])
+                rval = yamlfile.delete(params['key'], params['index'], params['value'])
 
             if rval[0] and params['src']:
                 yamlfile.write()

+ 48 - 12
roles/lib_openshift/library/oc_adm_router.py

@@ -34,6 +34,7 @@
 from __future__ import print_function
 import atexit
 import copy
+import fcntl
 import json
 import os
 import re
@@ -336,14 +337,35 @@ class Yedit(object):  # pragma: no cover
 
         return True
 
+    # pylint: disable=too-many-return-statements,too-many-branches
     @staticmethod
-    def remove_entry(data, key, sep='.'):
+    def remove_entry(data, key, index=None, value=None, sep='.'):
         ''' remove data at location key '''
         if key == '' and isinstance(data, dict):
-            data.clear()
+            if value is not None:
+                data.pop(value)
+            elif index is not None:
+                raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
+            else:
+                data.clear()
+
             return True
+
         elif key == '' and isinstance(data, list):
-            del data[:]
+            ind = None
+            if value is not None:
+                try:
+                    ind = data.index(value)
+                except ValueError:
+                    return False
+            elif index is not None:
+                ind = index
+            else:
+                del data[:]
+
+            if ind is not None:
+                data.pop(ind)
+
             return True
 
         if not (key and Yedit.valid_key(key, sep)) and \
@@ -458,7 +480,9 @@ class Yedit(object):  # pragma: no cover
         tmp_filename = filename + '.yedit'
 
         with open(tmp_filename, 'w') as yfd:
+            fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             yfd.write(contents)
+            fcntl.flock(yfd, fcntl.LOCK_UN)
 
         os.rename(tmp_filename, filename)
 
@@ -477,10 +501,16 @@ class Yedit(object):  # pragma: no cover
             pass
 
         # Try to use RoundTripDumper if supported.
-        try:
-            Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
-        except AttributeError:
-            Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        if self.content_type == 'yaml':
+            try:
+                Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+            except AttributeError:
+                Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        elif self.content_type == 'json':
+            Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
+        else:
+            raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
+                                 'Please specify a content_type of yaml or json.')
 
         return (True, self.yaml_dict)
 
@@ -528,7 +558,7 @@ class Yedit(object):  # pragma: no cover
 
                 # Try to use RoundTripLoader if supported.
                 try:
-                    self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+                    self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
                 except AttributeError:
                     self.yaml_dict = yaml.safe_load(contents)
 
@@ -587,7 +617,7 @@ class Yedit(object):  # pragma: no cover
 
         return (False, self.yaml_dict)
 
-    def delete(self, path):
+    def delete(self, path, index=None, value=None):
         ''' remove path from a dict'''
         try:
             entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
@@ -597,7 +627,7 @@ class Yedit(object):  # pragma: no cover
         if entry is None:
             return (False, self.yaml_dict)
 
-        result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+        result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
         if not result:
             return (False, self.yaml_dict)
 
@@ -773,7 +803,12 @@ class Yedit(object):  # pragma: no cover
 
         curr_value = invalue
         if val_type == 'yaml':
-            curr_value = yaml.load(invalue)
+            try:
+                # AUDIT:maybe-no-member makes sense due to different yaml libraries
+                # pylint: disable=maybe-no-member
+                curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
+            except AttributeError:
+                curr_value = yaml.safe_load(invalue)
         elif val_type == 'json':
             curr_value = json.loads(invalue)
 
@@ -842,6 +877,7 @@ class Yedit(object):  # pragma: no cover
         '''perform the idempotent crud operations'''
         yamlfile = Yedit(filename=params['src'],
                          backup=params['backup'],
+                         content_type=params['content_type'],
                          separator=params['separator'])
 
         state = params['state']
@@ -872,7 +908,7 @@ class Yedit(object):  # pragma: no cover
             if params['update']:
                 rval = yamlfile.pop(params['key'], params['value'])
             else:
-                rval = yamlfile.delete(params['key'])
+                rval = yamlfile.delete(params['key'], params['index'], params['value'])
 
             if rval[0] and params['src']:
                 yamlfile.write()

+ 48 - 12
roles/lib_openshift/library/oc_clusterrole.py

@@ -34,6 +34,7 @@
 from __future__ import print_function
 import atexit
 import copy
+import fcntl
 import json
 import os
 import re
@@ -185,14 +186,35 @@ class Yedit(object):  # pragma: no cover
 
         return True
 
+    # pylint: disable=too-many-return-statements,too-many-branches
     @staticmethod
-    def remove_entry(data, key, sep='.'):
+    def remove_entry(data, key, index=None, value=None, sep='.'):
         ''' remove data at location key '''
         if key == '' and isinstance(data, dict):
-            data.clear()
+            if value is not None:
+                data.pop(value)
+            elif index is not None:
+                raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
+            else:
+                data.clear()
+
             return True
+
         elif key == '' and isinstance(data, list):
-            del data[:]
+            ind = None
+            if value is not None:
+                try:
+                    ind = data.index(value)
+                except ValueError:
+                    return False
+            elif index is not None:
+                ind = index
+            else:
+                del data[:]
+
+            if ind is not None:
+                data.pop(ind)
+
             return True
 
         if not (key and Yedit.valid_key(key, sep)) and \
@@ -307,7 +329,9 @@ class Yedit(object):  # pragma: no cover
         tmp_filename = filename + '.yedit'
 
         with open(tmp_filename, 'w') as yfd:
+            fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             yfd.write(contents)
+            fcntl.flock(yfd, fcntl.LOCK_UN)
 
         os.rename(tmp_filename, filename)
 
@@ -326,10 +350,16 @@ class Yedit(object):  # pragma: no cover
             pass
 
         # Try to use RoundTripDumper if supported.
-        try:
-            Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
-        except AttributeError:
-            Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        if self.content_type == 'yaml':
+            try:
+                Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+            except AttributeError:
+                Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        elif self.content_type == 'json':
+            Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
+        else:
+            raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
+                                 'Please specify a content_type of yaml or json.')
 
         return (True, self.yaml_dict)
 
@@ -377,7 +407,7 @@ class Yedit(object):  # pragma: no cover
 
                 # Try to use RoundTripLoader if supported.
                 try:
-                    self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+                    self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
                 except AttributeError:
                     self.yaml_dict = yaml.safe_load(contents)
 
@@ -436,7 +466,7 @@ class Yedit(object):  # pragma: no cover
 
         return (False, self.yaml_dict)
 
-    def delete(self, path):
+    def delete(self, path, index=None, value=None):
         ''' remove path from a dict'''
         try:
             entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
@@ -446,7 +476,7 @@ class Yedit(object):  # pragma: no cover
         if entry is None:
             return (False, self.yaml_dict)
 
-        result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+        result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
         if not result:
             return (False, self.yaml_dict)
 
@@ -622,7 +652,12 @@ class Yedit(object):  # pragma: no cover
 
         curr_value = invalue
         if val_type == 'yaml':
-            curr_value = yaml.load(invalue)
+            try:
+                # AUDIT:maybe-no-member makes sense due to different yaml libraries
+                # pylint: disable=maybe-no-member
+                curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
+            except AttributeError:
+                curr_value = yaml.safe_load(invalue)
         elif val_type == 'json':
             curr_value = json.loads(invalue)
 
@@ -691,6 +726,7 @@ class Yedit(object):  # pragma: no cover
         '''perform the idempotent crud operations'''
         yamlfile = Yedit(filename=params['src'],
                          backup=params['backup'],
+                         content_type=params['content_type'],
                          separator=params['separator'])
 
         state = params['state']
@@ -721,7 +757,7 @@ class Yedit(object):  # pragma: no cover
             if params['update']:
                 rval = yamlfile.pop(params['key'], params['value'])
             else:
-                rval = yamlfile.delete(params['key'])
+                rval = yamlfile.delete(params['key'], params['index'], params['value'])
 
             if rval[0] and params['src']:
                 yamlfile.write()

+ 48 - 12
roles/lib_openshift/library/oc_configmap.py

@@ -34,6 +34,7 @@
 from __future__ import print_function
 import atexit
 import copy
+import fcntl
 import json
 import os
 import re
@@ -191,14 +192,35 @@ class Yedit(object):  # pragma: no cover
 
         return True
 
+    # pylint: disable=too-many-return-statements,too-many-branches
     @staticmethod
-    def remove_entry(data, key, sep='.'):
+    def remove_entry(data, key, index=None, value=None, sep='.'):
         ''' remove data at location key '''
         if key == '' and isinstance(data, dict):
-            data.clear()
+            if value is not None:
+                data.pop(value)
+            elif index is not None:
+                raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
+            else:
+                data.clear()
+
             return True
+
         elif key == '' and isinstance(data, list):
-            del data[:]
+            ind = None
+            if value is not None:
+                try:
+                    ind = data.index(value)
+                except ValueError:
+                    return False
+            elif index is not None:
+                ind = index
+            else:
+                del data[:]
+
+            if ind is not None:
+                data.pop(ind)
+
             return True
 
         if not (key and Yedit.valid_key(key, sep)) and \
@@ -313,7 +335,9 @@ class Yedit(object):  # pragma: no cover
         tmp_filename = filename + '.yedit'
 
         with open(tmp_filename, 'w') as yfd:
+            fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             yfd.write(contents)
+            fcntl.flock(yfd, fcntl.LOCK_UN)
 
         os.rename(tmp_filename, filename)
 
@@ -332,10 +356,16 @@ class Yedit(object):  # pragma: no cover
             pass
 
         # Try to use RoundTripDumper if supported.
-        try:
-            Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
-        except AttributeError:
-            Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        if self.content_type == 'yaml':
+            try:
+                Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+            except AttributeError:
+                Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        elif self.content_type == 'json':
+            Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
+        else:
+            raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
+                                 'Please specify a content_type of yaml or json.')
 
         return (True, self.yaml_dict)
 
@@ -383,7 +413,7 @@ class Yedit(object):  # pragma: no cover
 
                 # Try to use RoundTripLoader if supported.
                 try:
-                    self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+                    self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
                 except AttributeError:
                     self.yaml_dict = yaml.safe_load(contents)
 
@@ -442,7 +472,7 @@ class Yedit(object):  # pragma: no cover
 
         return (False, self.yaml_dict)
 
-    def delete(self, path):
+    def delete(self, path, index=None, value=None):
         ''' remove path from a dict'''
         try:
             entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
@@ -452,7 +482,7 @@ class Yedit(object):  # pragma: no cover
         if entry is None:
             return (False, self.yaml_dict)
 
-        result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+        result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
         if not result:
             return (False, self.yaml_dict)
 
@@ -628,7 +658,12 @@ class Yedit(object):  # pragma: no cover
 
         curr_value = invalue
         if val_type == 'yaml':
-            curr_value = yaml.load(invalue)
+            try:
+                # AUDIT:maybe-no-member makes sense due to different yaml libraries
+                # pylint: disable=maybe-no-member
+                curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
+            except AttributeError:
+                curr_value = yaml.safe_load(invalue)
         elif val_type == 'json':
             curr_value = json.loads(invalue)
 
@@ -697,6 +732,7 @@ class Yedit(object):  # pragma: no cover
         '''perform the idempotent crud operations'''
         yamlfile = Yedit(filename=params['src'],
                          backup=params['backup'],
+                         content_type=params['content_type'],
                          separator=params['separator'])
 
         state = params['state']
@@ -727,7 +763,7 @@ class Yedit(object):  # pragma: no cover
             if params['update']:
                 rval = yamlfile.pop(params['key'], params['value'])
             else:
-                rval = yamlfile.delete(params['key'])
+                rval = yamlfile.delete(params['key'], params['index'], params['value'])
 
             if rval[0] and params['src']:
                 yamlfile.write()

+ 48 - 12
roles/lib_openshift/library/oc_edit.py

@@ -34,6 +34,7 @@
 from __future__ import print_function
 import atexit
 import copy
+import fcntl
 import json
 import os
 import re
@@ -235,14 +236,35 @@ class Yedit(object):  # pragma: no cover
 
         return True
 
+    # pylint: disable=too-many-return-statements,too-many-branches
     @staticmethod
-    def remove_entry(data, key, sep='.'):
+    def remove_entry(data, key, index=None, value=None, sep='.'):
         ''' remove data at location key '''
         if key == '' and isinstance(data, dict):
-            data.clear()
+            if value is not None:
+                data.pop(value)
+            elif index is not None:
+                raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
+            else:
+                data.clear()
+
             return True
+
         elif key == '' and isinstance(data, list):
-            del data[:]
+            ind = None
+            if value is not None:
+                try:
+                    ind = data.index(value)
+                except ValueError:
+                    return False
+            elif index is not None:
+                ind = index
+            else:
+                del data[:]
+
+            if ind is not None:
+                data.pop(ind)
+
             return True
 
         if not (key and Yedit.valid_key(key, sep)) and \
@@ -357,7 +379,9 @@ class Yedit(object):  # pragma: no cover
         tmp_filename = filename + '.yedit'
 
         with open(tmp_filename, 'w') as yfd:
+            fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             yfd.write(contents)
+            fcntl.flock(yfd, fcntl.LOCK_UN)
 
         os.rename(tmp_filename, filename)
 
@@ -376,10 +400,16 @@ class Yedit(object):  # pragma: no cover
             pass
 
         # Try to use RoundTripDumper if supported.
-        try:
-            Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
-        except AttributeError:
-            Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        if self.content_type == 'yaml':
+            try:
+                Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+            except AttributeError:
+                Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        elif self.content_type == 'json':
+            Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
+        else:
+            raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
+                                 'Please specify a content_type of yaml or json.')
 
         return (True, self.yaml_dict)
 
@@ -427,7 +457,7 @@ class Yedit(object):  # pragma: no cover
 
                 # Try to use RoundTripLoader if supported.
                 try:
-                    self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+                    self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
                 except AttributeError:
                     self.yaml_dict = yaml.safe_load(contents)
 
@@ -486,7 +516,7 @@ class Yedit(object):  # pragma: no cover
 
         return (False, self.yaml_dict)
 
-    def delete(self, path):
+    def delete(self, path, index=None, value=None):
         ''' remove path from a dict'''
         try:
             entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
@@ -496,7 +526,7 @@ class Yedit(object):  # pragma: no cover
         if entry is None:
             return (False, self.yaml_dict)
 
-        result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+        result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
         if not result:
             return (False, self.yaml_dict)
 
@@ -672,7 +702,12 @@ class Yedit(object):  # pragma: no cover
 
         curr_value = invalue
         if val_type == 'yaml':
-            curr_value = yaml.load(invalue)
+            try:
+                # AUDIT:maybe-no-member makes sense due to different yaml libraries
+                # pylint: disable=maybe-no-member
+                curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
+            except AttributeError:
+                curr_value = yaml.safe_load(invalue)
         elif val_type == 'json':
             curr_value = json.loads(invalue)
 
@@ -741,6 +776,7 @@ class Yedit(object):  # pragma: no cover
         '''perform the idempotent crud operations'''
         yamlfile = Yedit(filename=params['src'],
                          backup=params['backup'],
+                         content_type=params['content_type'],
                          separator=params['separator'])
 
         state = params['state']
@@ -771,7 +807,7 @@ class Yedit(object):  # pragma: no cover
             if params['update']:
                 rval = yamlfile.pop(params['key'], params['value'])
             else:
-                rval = yamlfile.delete(params['key'])
+                rval = yamlfile.delete(params['key'], params['index'], params['value'])
 
             if rval[0] and params['src']:
                 yamlfile.write()

+ 48 - 12
roles/lib_openshift/library/oc_env.py

@@ -34,6 +34,7 @@
 from __future__ import print_function
 import atexit
 import copy
+import fcntl
 import json
 import os
 import re
@@ -202,14 +203,35 @@ class Yedit(object):  # pragma: no cover
 
         return True
 
+    # pylint: disable=too-many-return-statements,too-many-branches
     @staticmethod
-    def remove_entry(data, key, sep='.'):
+    def remove_entry(data, key, index=None, value=None, sep='.'):
         ''' remove data at location key '''
         if key == '' and isinstance(data, dict):
-            data.clear()
+            if value is not None:
+                data.pop(value)
+            elif index is not None:
+                raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
+            else:
+                data.clear()
+
             return True
+
         elif key == '' and isinstance(data, list):
-            del data[:]
+            ind = None
+            if value is not None:
+                try:
+                    ind = data.index(value)
+                except ValueError:
+                    return False
+            elif index is not None:
+                ind = index
+            else:
+                del data[:]
+
+            if ind is not None:
+                data.pop(ind)
+
             return True
 
         if not (key and Yedit.valid_key(key, sep)) and \
@@ -324,7 +346,9 @@ class Yedit(object):  # pragma: no cover
         tmp_filename = filename + '.yedit'
 
         with open(tmp_filename, 'w') as yfd:
+            fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             yfd.write(contents)
+            fcntl.flock(yfd, fcntl.LOCK_UN)
 
         os.rename(tmp_filename, filename)
 
@@ -343,10 +367,16 @@ class Yedit(object):  # pragma: no cover
             pass
 
         # Try to use RoundTripDumper if supported.
-        try:
-            Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
-        except AttributeError:
-            Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        if self.content_type == 'yaml':
+            try:
+                Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+            except AttributeError:
+                Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        elif self.content_type == 'json':
+            Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
+        else:
+            raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
+                                 'Please specify a content_type of yaml or json.')
 
         return (True, self.yaml_dict)
 
@@ -394,7 +424,7 @@ class Yedit(object):  # pragma: no cover
 
                 # Try to use RoundTripLoader if supported.
                 try:
-                    self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+                    self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
                 except AttributeError:
                     self.yaml_dict = yaml.safe_load(contents)
 
@@ -453,7 +483,7 @@ class Yedit(object):  # pragma: no cover
 
         return (False, self.yaml_dict)
 
-    def delete(self, path):
+    def delete(self, path, index=None, value=None):
         ''' remove path from a dict'''
         try:
             entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
@@ -463,7 +493,7 @@ class Yedit(object):  # pragma: no cover
         if entry is None:
             return (False, self.yaml_dict)
 
-        result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+        result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
         if not result:
             return (False, self.yaml_dict)
 
@@ -639,7 +669,12 @@ class Yedit(object):  # pragma: no cover
 
         curr_value = invalue
         if val_type == 'yaml':
-            curr_value = yaml.load(invalue)
+            try:
+                # AUDIT:maybe-no-member makes sense due to different yaml libraries
+                # pylint: disable=maybe-no-member
+                curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
+            except AttributeError:
+                curr_value = yaml.safe_load(invalue)
         elif val_type == 'json':
             curr_value = json.loads(invalue)
 
@@ -708,6 +743,7 @@ class Yedit(object):  # pragma: no cover
         '''perform the idempotent crud operations'''
         yamlfile = Yedit(filename=params['src'],
                          backup=params['backup'],
+                         content_type=params['content_type'],
                          separator=params['separator'])
 
         state = params['state']
@@ -738,7 +774,7 @@ class Yedit(object):  # pragma: no cover
             if params['update']:
                 rval = yamlfile.pop(params['key'], params['value'])
             else:
-                rval = yamlfile.delete(params['key'])
+                rval = yamlfile.delete(params['key'], params['index'], params['value'])
 
             if rval[0] and params['src']:
                 yamlfile.write()

+ 48 - 12
roles/lib_openshift/library/oc_group.py

@@ -34,6 +34,7 @@
 from __future__ import print_function
 import atexit
 import copy
+import fcntl
 import json
 import os
 import re
@@ -175,14 +176,35 @@ class Yedit(object):  # pragma: no cover
 
         return True
 
+    # pylint: disable=too-many-return-statements,too-many-branches
     @staticmethod
-    def remove_entry(data, key, sep='.'):
+    def remove_entry(data, key, index=None, value=None, sep='.'):
         ''' remove data at location key '''
         if key == '' and isinstance(data, dict):
-            data.clear()
+            if value is not None:
+                data.pop(value)
+            elif index is not None:
+                raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
+            else:
+                data.clear()
+
             return True
+
         elif key == '' and isinstance(data, list):
-            del data[:]
+            ind = None
+            if value is not None:
+                try:
+                    ind = data.index(value)
+                except ValueError:
+                    return False
+            elif index is not None:
+                ind = index
+            else:
+                del data[:]
+
+            if ind is not None:
+                data.pop(ind)
+
             return True
 
         if not (key and Yedit.valid_key(key, sep)) and \
@@ -297,7 +319,9 @@ class Yedit(object):  # pragma: no cover
         tmp_filename = filename + '.yedit'
 
         with open(tmp_filename, 'w') as yfd:
+            fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             yfd.write(contents)
+            fcntl.flock(yfd, fcntl.LOCK_UN)
 
         os.rename(tmp_filename, filename)
 
@@ -316,10 +340,16 @@ class Yedit(object):  # pragma: no cover
             pass
 
         # Try to use RoundTripDumper if supported.
-        try:
-            Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
-        except AttributeError:
-            Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        if self.content_type == 'yaml':
+            try:
+                Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+            except AttributeError:
+                Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        elif self.content_type == 'json':
+            Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
+        else:
+            raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
+                                 'Please specify a content_type of yaml or json.')
 
         return (True, self.yaml_dict)
 
@@ -367,7 +397,7 @@ class Yedit(object):  # pragma: no cover
 
                 # Try to use RoundTripLoader if supported.
                 try:
-                    self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+                    self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
                 except AttributeError:
                     self.yaml_dict = yaml.safe_load(contents)
 
@@ -426,7 +456,7 @@ class Yedit(object):  # pragma: no cover
 
         return (False, self.yaml_dict)
 
-    def delete(self, path):
+    def delete(self, path, index=None, value=None):
         ''' remove path from a dict'''
         try:
             entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
@@ -436,7 +466,7 @@ class Yedit(object):  # pragma: no cover
         if entry is None:
             return (False, self.yaml_dict)
 
-        result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+        result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
         if not result:
             return (False, self.yaml_dict)
 
@@ -612,7 +642,12 @@ class Yedit(object):  # pragma: no cover
 
         curr_value = invalue
         if val_type == 'yaml':
-            curr_value = yaml.load(invalue)
+            try:
+                # AUDIT:maybe-no-member makes sense due to different yaml libraries
+                # pylint: disable=maybe-no-member
+                curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
+            except AttributeError:
+                curr_value = yaml.safe_load(invalue)
         elif val_type == 'json':
             curr_value = json.loads(invalue)
 
@@ -681,6 +716,7 @@ class Yedit(object):  # pragma: no cover
         '''perform the idempotent crud operations'''
         yamlfile = Yedit(filename=params['src'],
                          backup=params['backup'],
+                         content_type=params['content_type'],
                          separator=params['separator'])
 
         state = params['state']
@@ -711,7 +747,7 @@ class Yedit(object):  # pragma: no cover
             if params['update']:
                 rval = yamlfile.pop(params['key'], params['value'])
             else:
-                rval = yamlfile.delete(params['key'])
+                rval = yamlfile.delete(params['key'], params['index'], params['value'])
 
             if rval[0] and params['src']:
                 yamlfile.write()

+ 48 - 12
roles/lib_openshift/library/oc_image.py

@@ -34,6 +34,7 @@
 from __future__ import print_function
 import atexit
 import copy
+import fcntl
 import json
 import os
 import re
@@ -194,14 +195,35 @@ class Yedit(object):  # pragma: no cover
 
         return True
 
+    # pylint: disable=too-many-return-statements,too-many-branches
     @staticmethod
-    def remove_entry(data, key, sep='.'):
+    def remove_entry(data, key, index=None, value=None, sep='.'):
         ''' remove data at location key '''
         if key == '' and isinstance(data, dict):
-            data.clear()
+            if value is not None:
+                data.pop(value)
+            elif index is not None:
+                raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
+            else:
+                data.clear()
+
             return True
+
         elif key == '' and isinstance(data, list):
-            del data[:]
+            ind = None
+            if value is not None:
+                try:
+                    ind = data.index(value)
+                except ValueError:
+                    return False
+            elif index is not None:
+                ind = index
+            else:
+                del data[:]
+
+            if ind is not None:
+                data.pop(ind)
+
             return True
 
         if not (key and Yedit.valid_key(key, sep)) and \
@@ -316,7 +338,9 @@ class Yedit(object):  # pragma: no cover
         tmp_filename = filename + '.yedit'
 
         with open(tmp_filename, 'w') as yfd:
+            fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             yfd.write(contents)
+            fcntl.flock(yfd, fcntl.LOCK_UN)
 
         os.rename(tmp_filename, filename)
 
@@ -335,10 +359,16 @@ class Yedit(object):  # pragma: no cover
             pass
 
         # Try to use RoundTripDumper if supported.
-        try:
-            Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
-        except AttributeError:
-            Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        if self.content_type == 'yaml':
+            try:
+                Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+            except AttributeError:
+                Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        elif self.content_type == 'json':
+            Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
+        else:
+            raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
+                                 'Please specify a content_type of yaml or json.')
 
         return (True, self.yaml_dict)
 
@@ -386,7 +416,7 @@ class Yedit(object):  # pragma: no cover
 
                 # Try to use RoundTripLoader if supported.
                 try:
-                    self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+                    self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
                 except AttributeError:
                     self.yaml_dict = yaml.safe_load(contents)
 
@@ -445,7 +475,7 @@ class Yedit(object):  # pragma: no cover
 
         return (False, self.yaml_dict)
 
-    def delete(self, path):
+    def delete(self, path, index=None, value=None):
         ''' remove path from a dict'''
         try:
             entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
@@ -455,7 +485,7 @@ class Yedit(object):  # pragma: no cover
         if entry is None:
             return (False, self.yaml_dict)
 
-        result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+        result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
         if not result:
             return (False, self.yaml_dict)
 
@@ -631,7 +661,12 @@ class Yedit(object):  # pragma: no cover
 
         curr_value = invalue
         if val_type == 'yaml':
-            curr_value = yaml.load(invalue)
+            try:
+                # AUDIT:maybe-no-member makes sense due to different yaml libraries
+                # pylint: disable=maybe-no-member
+                curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
+            except AttributeError:
+                curr_value = yaml.safe_load(invalue)
         elif val_type == 'json':
             curr_value = json.loads(invalue)
 
@@ -700,6 +735,7 @@ class Yedit(object):  # pragma: no cover
         '''perform the idempotent crud operations'''
         yamlfile = Yedit(filename=params['src'],
                          backup=params['backup'],
+                         content_type=params['content_type'],
                          separator=params['separator'])
 
         state = params['state']
@@ -730,7 +766,7 @@ class Yedit(object):  # pragma: no cover
             if params['update']:
                 rval = yamlfile.pop(params['key'], params['value'])
             else:
-                rval = yamlfile.delete(params['key'])
+                rval = yamlfile.delete(params['key'], params['index'], params['value'])
 
             if rval[0] and params['src']:
                 yamlfile.write()

+ 48 - 12
roles/lib_openshift/library/oc_label.py

@@ -34,6 +34,7 @@
 from __future__ import print_function
 import atexit
 import copy
+import fcntl
 import json
 import os
 import re
@@ -211,14 +212,35 @@ class Yedit(object):  # pragma: no cover
 
         return True
 
+    # pylint: disable=too-many-return-statements,too-many-branches
     @staticmethod
-    def remove_entry(data, key, sep='.'):
+    def remove_entry(data, key, index=None, value=None, sep='.'):
         ''' remove data at location key '''
         if key == '' and isinstance(data, dict):
-            data.clear()
+            if value is not None:
+                data.pop(value)
+            elif index is not None:
+                raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
+            else:
+                data.clear()
+
             return True
+
         elif key == '' and isinstance(data, list):
-            del data[:]
+            ind = None
+            if value is not None:
+                try:
+                    ind = data.index(value)
+                except ValueError:
+                    return False
+            elif index is not None:
+                ind = index
+            else:
+                del data[:]
+
+            if ind is not None:
+                data.pop(ind)
+
             return True
 
         if not (key and Yedit.valid_key(key, sep)) and \
@@ -333,7 +355,9 @@ class Yedit(object):  # pragma: no cover
         tmp_filename = filename + '.yedit'
 
         with open(tmp_filename, 'w') as yfd:
+            fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             yfd.write(contents)
+            fcntl.flock(yfd, fcntl.LOCK_UN)
 
         os.rename(tmp_filename, filename)
 
@@ -352,10 +376,16 @@ class Yedit(object):  # pragma: no cover
             pass
 
         # Try to use RoundTripDumper if supported.
-        try:
-            Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
-        except AttributeError:
-            Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        if self.content_type == 'yaml':
+            try:
+                Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+            except AttributeError:
+                Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        elif self.content_type == 'json':
+            Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
+        else:
+            raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
+                                 'Please specify a content_type of yaml or json.')
 
         return (True, self.yaml_dict)
 
@@ -403,7 +433,7 @@ class Yedit(object):  # pragma: no cover
 
                 # Try to use RoundTripLoader if supported.
                 try:
-                    self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+                    self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
                 except AttributeError:
                     self.yaml_dict = yaml.safe_load(contents)
 
@@ -462,7 +492,7 @@ class Yedit(object):  # pragma: no cover
 
         return (False, self.yaml_dict)
 
-    def delete(self, path):
+    def delete(self, path, index=None, value=None):
         ''' remove path from a dict'''
         try:
             entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
@@ -472,7 +502,7 @@ class Yedit(object):  # pragma: no cover
         if entry is None:
             return (False, self.yaml_dict)
 
-        result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+        result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
         if not result:
             return (False, self.yaml_dict)
 
@@ -648,7 +678,12 @@ class Yedit(object):  # pragma: no cover
 
         curr_value = invalue
         if val_type == 'yaml':
-            curr_value = yaml.load(invalue)
+            try:
+                # AUDIT:maybe-no-member makes sense due to different yaml libraries
+                # pylint: disable=maybe-no-member
+                curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
+            except AttributeError:
+                curr_value = yaml.safe_load(invalue)
         elif val_type == 'json':
             curr_value = json.loads(invalue)
 
@@ -717,6 +752,7 @@ class Yedit(object):  # pragma: no cover
         '''perform the idempotent crud operations'''
         yamlfile = Yedit(filename=params['src'],
                          backup=params['backup'],
+                         content_type=params['content_type'],
                          separator=params['separator'])
 
         state = params['state']
@@ -747,7 +783,7 @@ class Yedit(object):  # pragma: no cover
             if params['update']:
                 rval = yamlfile.pop(params['key'], params['value'])
             else:
-                rval = yamlfile.delete(params['key'])
+                rval = yamlfile.delete(params['key'], params['index'], params['value'])
 
             if rval[0] and params['src']:
                 yamlfile.write()

+ 48 - 12
roles/lib_openshift/library/oc_obj.py

@@ -34,6 +34,7 @@
 from __future__ import print_function
 import atexit
 import copy
+import fcntl
 import json
 import os
 import re
@@ -214,14 +215,35 @@ class Yedit(object):  # pragma: no cover
 
         return True
 
+    # pylint: disable=too-many-return-statements,too-many-branches
     @staticmethod
-    def remove_entry(data, key, sep='.'):
+    def remove_entry(data, key, index=None, value=None, sep='.'):
         ''' remove data at location key '''
         if key == '' and isinstance(data, dict):
-            data.clear()
+            if value is not None:
+                data.pop(value)
+            elif index is not None:
+                raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
+            else:
+                data.clear()
+
             return True
+
         elif key == '' and isinstance(data, list):
-            del data[:]
+            ind = None
+            if value is not None:
+                try:
+                    ind = data.index(value)
+                except ValueError:
+                    return False
+            elif index is not None:
+                ind = index
+            else:
+                del data[:]
+
+            if ind is not None:
+                data.pop(ind)
+
             return True
 
         if not (key and Yedit.valid_key(key, sep)) and \
@@ -336,7 +358,9 @@ class Yedit(object):  # pragma: no cover
         tmp_filename = filename + '.yedit'
 
         with open(tmp_filename, 'w') as yfd:
+            fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             yfd.write(contents)
+            fcntl.flock(yfd, fcntl.LOCK_UN)
 
         os.rename(tmp_filename, filename)
 
@@ -355,10 +379,16 @@ class Yedit(object):  # pragma: no cover
             pass
 
         # Try to use RoundTripDumper if supported.
-        try:
-            Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
-        except AttributeError:
-            Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        if self.content_type == 'yaml':
+            try:
+                Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+            except AttributeError:
+                Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        elif self.content_type == 'json':
+            Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
+        else:
+            raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
+                                 'Please specify a content_type of yaml or json.')
 
         return (True, self.yaml_dict)
 
@@ -406,7 +436,7 @@ class Yedit(object):  # pragma: no cover
 
                 # Try to use RoundTripLoader if supported.
                 try:
-                    self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+                    self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
                 except AttributeError:
                     self.yaml_dict = yaml.safe_load(contents)
 
@@ -465,7 +495,7 @@ class Yedit(object):  # pragma: no cover
 
         return (False, self.yaml_dict)
 
-    def delete(self, path):
+    def delete(self, path, index=None, value=None):
         ''' remove path from a dict'''
         try:
             entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
@@ -475,7 +505,7 @@ class Yedit(object):  # pragma: no cover
         if entry is None:
             return (False, self.yaml_dict)
 
-        result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+        result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
         if not result:
             return (False, self.yaml_dict)
 
@@ -651,7 +681,12 @@ class Yedit(object):  # pragma: no cover
 
         curr_value = invalue
         if val_type == 'yaml':
-            curr_value = yaml.load(invalue)
+            try:
+                # AUDIT:maybe-no-member makes sense due to different yaml libraries
+                # pylint: disable=maybe-no-member
+                curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
+            except AttributeError:
+                curr_value = yaml.safe_load(invalue)
         elif val_type == 'json':
             curr_value = json.loads(invalue)
 
@@ -720,6 +755,7 @@ class Yedit(object):  # pragma: no cover
         '''perform the idempotent crud operations'''
         yamlfile = Yedit(filename=params['src'],
                          backup=params['backup'],
+                         content_type=params['content_type'],
                          separator=params['separator'])
 
         state = params['state']
@@ -750,7 +786,7 @@ class Yedit(object):  # pragma: no cover
             if params['update']:
                 rval = yamlfile.pop(params['key'], params['value'])
             else:
-                rval = yamlfile.delete(params['key'])
+                rval = yamlfile.delete(params['key'], params['index'], params['value'])
 
             if rval[0] and params['src']:
                 yamlfile.write()

+ 48 - 12
roles/lib_openshift/library/oc_objectvalidator.py

@@ -34,6 +34,7 @@
 from __future__ import print_function
 import atexit
 import copy
+import fcntl
 import json
 import os
 import re
@@ -146,14 +147,35 @@ class Yedit(object):  # pragma: no cover
 
         return True
 
+    # pylint: disable=too-many-return-statements,too-many-branches
     @staticmethod
-    def remove_entry(data, key, sep='.'):
+    def remove_entry(data, key, index=None, value=None, sep='.'):
         ''' remove data at location key '''
         if key == '' and isinstance(data, dict):
-            data.clear()
+            if value is not None:
+                data.pop(value)
+            elif index is not None:
+                raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
+            else:
+                data.clear()
+
             return True
+
         elif key == '' and isinstance(data, list):
-            del data[:]
+            ind = None
+            if value is not None:
+                try:
+                    ind = data.index(value)
+                except ValueError:
+                    return False
+            elif index is not None:
+                ind = index
+            else:
+                del data[:]
+
+            if ind is not None:
+                data.pop(ind)
+
             return True
 
         if not (key and Yedit.valid_key(key, sep)) and \
@@ -268,7 +290,9 @@ class Yedit(object):  # pragma: no cover
         tmp_filename = filename + '.yedit'
 
         with open(tmp_filename, 'w') as yfd:
+            fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             yfd.write(contents)
+            fcntl.flock(yfd, fcntl.LOCK_UN)
 
         os.rename(tmp_filename, filename)
 
@@ -287,10 +311,16 @@ class Yedit(object):  # pragma: no cover
             pass
 
         # Try to use RoundTripDumper if supported.
-        try:
-            Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
-        except AttributeError:
-            Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        if self.content_type == 'yaml':
+            try:
+                Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+            except AttributeError:
+                Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        elif self.content_type == 'json':
+            Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
+        else:
+            raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
+                                 'Please specify a content_type of yaml or json.')
 
         return (True, self.yaml_dict)
 
@@ -338,7 +368,7 @@ class Yedit(object):  # pragma: no cover
 
                 # Try to use RoundTripLoader if supported.
                 try:
-                    self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+                    self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
                 except AttributeError:
                     self.yaml_dict = yaml.safe_load(contents)
 
@@ -397,7 +427,7 @@ class Yedit(object):  # pragma: no cover
 
         return (False, self.yaml_dict)
 
-    def delete(self, path):
+    def delete(self, path, index=None, value=None):
         ''' remove path from a dict'''
         try:
             entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
@@ -407,7 +437,7 @@ class Yedit(object):  # pragma: no cover
         if entry is None:
             return (False, self.yaml_dict)
 
-        result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+        result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
         if not result:
             return (False, self.yaml_dict)
 
@@ -583,7 +613,12 @@ class Yedit(object):  # pragma: no cover
 
         curr_value = invalue
         if val_type == 'yaml':
-            curr_value = yaml.load(invalue)
+            try:
+                # AUDIT:maybe-no-member makes sense due to different yaml libraries
+                # pylint: disable=maybe-no-member
+                curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
+            except AttributeError:
+                curr_value = yaml.safe_load(invalue)
         elif val_type == 'json':
             curr_value = json.loads(invalue)
 
@@ -652,6 +687,7 @@ class Yedit(object):  # pragma: no cover
         '''perform the idempotent crud operations'''
         yamlfile = Yedit(filename=params['src'],
                          backup=params['backup'],
+                         content_type=params['content_type'],
                          separator=params['separator'])
 
         state = params['state']
@@ -682,7 +718,7 @@ class Yedit(object):  # pragma: no cover
             if params['update']:
                 rval = yamlfile.pop(params['key'], params['value'])
             else:
-                rval = yamlfile.delete(params['key'])
+                rval = yamlfile.delete(params['key'], params['index'], params['value'])
 
             if rval[0] and params['src']:
                 yamlfile.write()

+ 48 - 12
roles/lib_openshift/library/oc_process.py

@@ -34,6 +34,7 @@
 from __future__ import print_function
 import atexit
 import copy
+import fcntl
 import json
 import os
 import re
@@ -203,14 +204,35 @@ class Yedit(object):  # pragma: no cover
 
         return True
 
+    # pylint: disable=too-many-return-statements,too-many-branches
     @staticmethod
-    def remove_entry(data, key, sep='.'):
+    def remove_entry(data, key, index=None, value=None, sep='.'):
         ''' remove data at location key '''
         if key == '' and isinstance(data, dict):
-            data.clear()
+            if value is not None:
+                data.pop(value)
+            elif index is not None:
+                raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
+            else:
+                data.clear()
+
             return True
+
         elif key == '' and isinstance(data, list):
-            del data[:]
+            ind = None
+            if value is not None:
+                try:
+                    ind = data.index(value)
+                except ValueError:
+                    return False
+            elif index is not None:
+                ind = index
+            else:
+                del data[:]
+
+            if ind is not None:
+                data.pop(ind)
+
             return True
 
         if not (key and Yedit.valid_key(key, sep)) and \
@@ -325,7 +347,9 @@ class Yedit(object):  # pragma: no cover
         tmp_filename = filename + '.yedit'
 
         with open(tmp_filename, 'w') as yfd:
+            fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             yfd.write(contents)
+            fcntl.flock(yfd, fcntl.LOCK_UN)
 
         os.rename(tmp_filename, filename)
 
@@ -344,10 +368,16 @@ class Yedit(object):  # pragma: no cover
             pass
 
         # Try to use RoundTripDumper if supported.
-        try:
-            Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
-        except AttributeError:
-            Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        if self.content_type == 'yaml':
+            try:
+                Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+            except AttributeError:
+                Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        elif self.content_type == 'json':
+            Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
+        else:
+            raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
+                                 'Please specify a content_type of yaml or json.')
 
         return (True, self.yaml_dict)
 
@@ -395,7 +425,7 @@ class Yedit(object):  # pragma: no cover
 
                 # Try to use RoundTripLoader if supported.
                 try:
-                    self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+                    self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
                 except AttributeError:
                     self.yaml_dict = yaml.safe_load(contents)
 
@@ -454,7 +484,7 @@ class Yedit(object):  # pragma: no cover
 
         return (False, self.yaml_dict)
 
-    def delete(self, path):
+    def delete(self, path, index=None, value=None):
         ''' remove path from a dict'''
         try:
             entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
@@ -464,7 +494,7 @@ class Yedit(object):  # pragma: no cover
         if entry is None:
             return (False, self.yaml_dict)
 
-        result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+        result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
         if not result:
             return (False, self.yaml_dict)
 
@@ -640,7 +670,12 @@ class Yedit(object):  # pragma: no cover
 
         curr_value = invalue
         if val_type == 'yaml':
-            curr_value = yaml.load(invalue)
+            try:
+                # AUDIT:maybe-no-member makes sense due to different yaml libraries
+                # pylint: disable=maybe-no-member
+                curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
+            except AttributeError:
+                curr_value = yaml.safe_load(invalue)
         elif val_type == 'json':
             curr_value = json.loads(invalue)
 
@@ -709,6 +744,7 @@ class Yedit(object):  # pragma: no cover
         '''perform the idempotent crud operations'''
         yamlfile = Yedit(filename=params['src'],
                          backup=params['backup'],
+                         content_type=params['content_type'],
                          separator=params['separator'])
 
         state = params['state']
@@ -739,7 +775,7 @@ class Yedit(object):  # pragma: no cover
             if params['update']:
                 rval = yamlfile.pop(params['key'], params['value'])
             else:
-                rval = yamlfile.delete(params['key'])
+                rval = yamlfile.delete(params['key'], params['index'], params['value'])
 
             if rval[0] and params['src']:
                 yamlfile.write()

+ 48 - 12
roles/lib_openshift/library/oc_project.py

@@ -34,6 +34,7 @@
 from __future__ import print_function
 import atexit
 import copy
+import fcntl
 import json
 import os
 import re
@@ -200,14 +201,35 @@ class Yedit(object):  # pragma: no cover
 
         return True
 
+    # pylint: disable=too-many-return-statements,too-many-branches
     @staticmethod
-    def remove_entry(data, key, sep='.'):
+    def remove_entry(data, key, index=None, value=None, sep='.'):
         ''' remove data at location key '''
         if key == '' and isinstance(data, dict):
-            data.clear()
+            if value is not None:
+                data.pop(value)
+            elif index is not None:
+                raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
+            else:
+                data.clear()
+
             return True
+
         elif key == '' and isinstance(data, list):
-            del data[:]
+            ind = None
+            if value is not None:
+                try:
+                    ind = data.index(value)
+                except ValueError:
+                    return False
+            elif index is not None:
+                ind = index
+            else:
+                del data[:]
+
+            if ind is not None:
+                data.pop(ind)
+
             return True
 
         if not (key and Yedit.valid_key(key, sep)) and \
@@ -322,7 +344,9 @@ class Yedit(object):  # pragma: no cover
         tmp_filename = filename + '.yedit'
 
         with open(tmp_filename, 'w') as yfd:
+            fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             yfd.write(contents)
+            fcntl.flock(yfd, fcntl.LOCK_UN)
 
         os.rename(tmp_filename, filename)
 
@@ -341,10 +365,16 @@ class Yedit(object):  # pragma: no cover
             pass
 
         # Try to use RoundTripDumper if supported.
-        try:
-            Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
-        except AttributeError:
-            Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        if self.content_type == 'yaml':
+            try:
+                Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+            except AttributeError:
+                Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        elif self.content_type == 'json':
+            Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
+        else:
+            raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
+                                 'Please specify a content_type of yaml or json.')
 
         return (True, self.yaml_dict)
 
@@ -392,7 +422,7 @@ class Yedit(object):  # pragma: no cover
 
                 # Try to use RoundTripLoader if supported.
                 try:
-                    self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+                    self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
                 except AttributeError:
                     self.yaml_dict = yaml.safe_load(contents)
 
@@ -451,7 +481,7 @@ class Yedit(object):  # pragma: no cover
 
         return (False, self.yaml_dict)
 
-    def delete(self, path):
+    def delete(self, path, index=None, value=None):
         ''' remove path from a dict'''
         try:
             entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
@@ -461,7 +491,7 @@ class Yedit(object):  # pragma: no cover
         if entry is None:
             return (False, self.yaml_dict)
 
-        result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+        result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
         if not result:
             return (False, self.yaml_dict)
 
@@ -637,7 +667,12 @@ class Yedit(object):  # pragma: no cover
 
         curr_value = invalue
         if val_type == 'yaml':
-            curr_value = yaml.load(invalue)
+            try:
+                # AUDIT:maybe-no-member makes sense due to different yaml libraries
+                # pylint: disable=maybe-no-member
+                curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
+            except AttributeError:
+                curr_value = yaml.safe_load(invalue)
         elif val_type == 'json':
             curr_value = json.loads(invalue)
 
@@ -706,6 +741,7 @@ class Yedit(object):  # pragma: no cover
         '''perform the idempotent crud operations'''
         yamlfile = Yedit(filename=params['src'],
                          backup=params['backup'],
+                         content_type=params['content_type'],
                          separator=params['separator'])
 
         state = params['state']
@@ -736,7 +772,7 @@ class Yedit(object):  # pragma: no cover
             if params['update']:
                 rval = yamlfile.pop(params['key'], params['value'])
             else:
-                rval = yamlfile.delete(params['key'])
+                rval = yamlfile.delete(params['key'], params['index'], params['value'])
 
             if rval[0] and params['src']:
                 yamlfile.write()

+ 48 - 12
roles/lib_openshift/library/oc_pvc.py

@@ -34,6 +34,7 @@
 from __future__ import print_function
 import atexit
 import copy
+import fcntl
 import json
 import os
 import re
@@ -207,14 +208,35 @@ class Yedit(object):  # pragma: no cover
 
         return True
 
+    # pylint: disable=too-many-return-statements,too-many-branches
     @staticmethod
-    def remove_entry(data, key, sep='.'):
+    def remove_entry(data, key, index=None, value=None, sep='.'):
         ''' remove data at location key '''
         if key == '' and isinstance(data, dict):
-            data.clear()
+            if value is not None:
+                data.pop(value)
+            elif index is not None:
+                raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
+            else:
+                data.clear()
+
             return True
+
         elif key == '' and isinstance(data, list):
-            del data[:]
+            ind = None
+            if value is not None:
+                try:
+                    ind = data.index(value)
+                except ValueError:
+                    return False
+            elif index is not None:
+                ind = index
+            else:
+                del data[:]
+
+            if ind is not None:
+                data.pop(ind)
+
             return True
 
         if not (key and Yedit.valid_key(key, sep)) and \
@@ -329,7 +351,9 @@ class Yedit(object):  # pragma: no cover
         tmp_filename = filename + '.yedit'
 
         with open(tmp_filename, 'w') as yfd:
+            fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             yfd.write(contents)
+            fcntl.flock(yfd, fcntl.LOCK_UN)
 
         os.rename(tmp_filename, filename)
 
@@ -348,10 +372,16 @@ class Yedit(object):  # pragma: no cover
             pass
 
         # Try to use RoundTripDumper if supported.
-        try:
-            Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
-        except AttributeError:
-            Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        if self.content_type == 'yaml':
+            try:
+                Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+            except AttributeError:
+                Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+        elif self.content_type == 'json':
+            Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
+        else:
+            raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
+                                 'Please specify a content_type of yaml or json.')
 
         return (True, self.yaml_dict)
 
@@ -399,7 +429,7 @@ class Yedit(object):  # pragma: no cover
 
                 # Try to use RoundTripLoader if supported.
                 try:
-                    self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+                    self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
                 except AttributeError:
                     self.yaml_dict = yaml.safe_load(contents)
 
@@ -458,7 +488,7 @@ class Yedit(object):  # pragma: no cover
 
         return (False, self.yaml_dict)
 
-    def delete(self, path):
+    def delete(self, path, index=None, value=None):
         ''' remove path from a dict'''
         try:
             entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
@@ -468,7 +498,7 @@ class Yedit(object):  # pragma: no cover
         if entry is None:
             return (False, self.yaml_dict)
 
-        result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+        result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
         if not result:
             return (False, self.yaml_dict)
 
@@ -644,7 +674,12 @@ class Yedit(object):  # pragma: no cover
 
         curr_value = invalue
         if val_type == 'yaml':
-            curr_value = yaml.load(invalue)
+            try:
+                # AUDIT:maybe-no-member makes sense due to different yaml libraries
+                # pylint: disable=maybe-no-member
+                curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
+            except AttributeError:
+                curr_value = yaml.safe_load(invalue)
         elif val_type == 'json':
             curr_value = json.loads(invalue)
 
@@ -713,6 +748,7 @@ class Yedit(object):  # pragma: no cover
         '''perform the idempotent crud operations'''
         yamlfile = Yedit(filename=params['src'],
                          backup=params['backup'],
+                         content_type=params['content_type'],
                          separator=params['separator'])
 
         state = params['state']
@@ -743,7 +779,7 @@ class Yedit(object):  # pragma: no cover
             if params['update']:
                 rval = yamlfile.pop(params['key'], params['value'])
             else:
-                rval = yamlfile.delete(params['key'])
+                rval = yamlfile.delete(params['key'], params['index'], params['value'])
 
             if rval[0] and params['src']:
                 yamlfile.write()

+ 0 - 0
roles/lib_openshift/library/oc_route.py


Some files were not shown because too many files changed in this diff