From 1b514a153a14e65a5d8f26e7032eb97d919030ef Mon Sep 17 00:00:00 2001 From: SirishaGopigiri Date: Thu, 6 Aug 2020 20:01:28 +0530 Subject: [PATCH] Merging all branches --- README.md | 43 ++++++- airship-host-config/README.md | 37 ++++++ airship-host-config/build/Dockerfile | 17 ++- airship-host-config/build/ansible.cfg | 3 +- airship-host-config/create_labels.sh | 27 ++++- .../demo_examples/example.yaml | 6 - .../demo_examples/example3.yaml | 11 -- .../demo_examples/example4.yaml | 12 -- .../demo_examples/example5.yaml | 12 -- ...example1.yaml => example_host_groups.yaml} | 7 +- .../example_match_host_groups.yaml | 17 +++ .../demo_examples/example_max_percentage.yaml | 14 +++ .../demo_examples/example_parallel.yaml | 12 ++ ...{example2.yaml => example_sequential.yaml} | 9 +- .../example_sequential_match_host_groups.yaml | 17 +++ .../example_stop_on_failure.yaml | 13 ++ .../demo_examples/example_sysctl_ulimit.yaml | 23 ++++ .../deploy/cluster_role_binding.yaml | 2 +- ...tconfig.airshipit.org_hostconfigs_crd.yaml | 85 ++++++++++++- ....airshipit.org_v1alpha1_hostconfig_cr.yaml | 7 -- airship-host-config/deploy/operator.yaml | 16 ++- airship-host-config/deploy/role.yaml | 3 + .../install_ssh_private_key.sh | 24 ++++ .../inventory/dynamic_inventory.py | 88 ++++++++++---- .../molecule/cluster/converge.yml | 24 ---- .../molecule/cluster/create.yml | 6 - .../molecule/cluster/destroy.yml | 34 ------ .../molecule/cluster/molecule.yml | 35 ------ .../molecule/cluster/prepare.yml | 31 ----- .../molecule/cluster/verify.yml | 35 ------ .../molecule/default/converge.yml | 6 - .../molecule/default/molecule.yml | 45 ------- .../molecule/default/prepare.yml | 27 ----- .../molecule/default/verify.yml | 18 --- .../molecule/templates/operator.yaml.j2 | 40 ------- .../molecule/test-local/converge.yml | 42 ------- .../molecule/test-local/molecule.yml | 47 -------- .../molecule/test-local/prepare.yml | 3 - .../molecule/test-local/verify.yml | 2 - airship-host-config/playbook.yaml | 62 ---------- .../playbooks/create_playbook.yaml | 87 ++++++++++++++ .../playbooks/delete_playbook.yaml | 10 ++ .../callback/hostconfig_k8_cr_status.py | 112 ++++++++++++++++++ .../plugins/filter/host_config_serial.py | 27 ----- .../filter/host_config_serial_strategy.py | 30 ----- .../plugins/filter/hostconfig_host_groups.py | 88 ++++++++++++++ .../filter/hostconfig_host_groups_to_list.py | 25 ++++ .../filter/hostconfig_hosts_parallel.py | 42 +++++++ .../plugins/filter/hostconfig_sequential.py | 26 ++++ airship-host-config/requirements.yml | 4 +- .../roles/hostconfig/README.md | 43 ------- .../roles/hostconfig/defaults/main.yml | 3 - .../roles/hostconfig/handlers/main.yml | 2 - .../roles/hostconfig/meta/main.yml | 64 ---------- .../roles/hostconfig/tasks/main.yml | 25 ---- .../roles/hostconfig/vars/main.yml | 2 - .../roles/setvariables/tasks/main.yml | 74 ++++++++++++ .../roles/sysctl/tasks/main.yml | 15 +++ .../roles/ulimit/tasks/main.yml | 14 +++ airship-host-config/setup.sh | 2 + airship-host-config/watches.yaml | 6 +- docs/CR_creation_flow.png | Bin 0 -> 32955 bytes docs/Deployment_Architecture.png | Bin 0 -> 41430 bytes docs/Overview.md | 112 ++++++++++++++++++ docs/deployment_flow.png | Bin 0 -> 20487 bytes kubernetes/README.md | 34 +++++- kubernetes/Vagrantfile | 90 ++++++++++++-- kubernetes/haproxy.sh | 73 ++++++++++++ 68 files changed, 1196 insertions(+), 776 deletions(-) create mode 100644 airship-host-config/README.md delete mode 100644 airship-host-config/demo_examples/example.yaml delete mode 100644 airship-host-config/demo_examples/example3.yaml delete mode 100644 airship-host-config/demo_examples/example4.yaml delete mode 100644 airship-host-config/demo_examples/example5.yaml rename airship-host-config/demo_examples/{example1.yaml => example_host_groups.yaml} (60%) create mode 100644 airship-host-config/demo_examples/example_match_host_groups.yaml create mode 100644 airship-host-config/demo_examples/example_max_percentage.yaml create mode 100644 airship-host-config/demo_examples/example_parallel.yaml rename airship-host-config/demo_examples/{example2.yaml => example_sequential.yaml} (57%) create mode 100644 airship-host-config/demo_examples/example_sequential_match_host_groups.yaml create mode 100644 airship-host-config/demo_examples/example_stop_on_failure.yaml create mode 100644 airship-host-config/demo_examples/example_sysctl_ulimit.yaml delete mode 100644 airship-host-config/deploy/crds/hostconfig.airshipit.org_v1alpha1_hostconfig_cr.yaml create mode 100755 airship-host-config/install_ssh_private_key.sh delete mode 100644 airship-host-config/molecule/cluster/converge.yml delete mode 100644 airship-host-config/molecule/cluster/create.yml delete mode 100644 airship-host-config/molecule/cluster/destroy.yml delete mode 100644 airship-host-config/molecule/cluster/molecule.yml delete mode 100644 airship-host-config/molecule/cluster/prepare.yml delete mode 100644 airship-host-config/molecule/cluster/verify.yml delete mode 100644 airship-host-config/molecule/default/converge.yml delete mode 100644 airship-host-config/molecule/default/molecule.yml delete mode 100644 airship-host-config/molecule/default/prepare.yml delete mode 100644 airship-host-config/molecule/default/verify.yml delete mode 100644 airship-host-config/molecule/templates/operator.yaml.j2 delete mode 100644 airship-host-config/molecule/test-local/converge.yml delete mode 100644 airship-host-config/molecule/test-local/molecule.yml delete mode 100644 airship-host-config/molecule/test-local/prepare.yml delete mode 100644 airship-host-config/molecule/test-local/verify.yml delete mode 100644 airship-host-config/playbook.yaml create mode 100644 airship-host-config/playbooks/create_playbook.yaml create mode 100644 airship-host-config/playbooks/delete_playbook.yaml create mode 100644 airship-host-config/plugins/callback/hostconfig_k8_cr_status.py delete mode 100644 airship-host-config/plugins/filter/host_config_serial.py delete mode 100644 airship-host-config/plugins/filter/host_config_serial_strategy.py create mode 100644 airship-host-config/plugins/filter/hostconfig_host_groups.py create mode 100644 airship-host-config/plugins/filter/hostconfig_host_groups_to_list.py create mode 100644 airship-host-config/plugins/filter/hostconfig_hosts_parallel.py create mode 100644 airship-host-config/plugins/filter/hostconfig_sequential.py delete mode 100644 airship-host-config/roles/hostconfig/README.md delete mode 100644 airship-host-config/roles/hostconfig/defaults/main.yml delete mode 100644 airship-host-config/roles/hostconfig/handlers/main.yml delete mode 100644 airship-host-config/roles/hostconfig/meta/main.yml delete mode 100644 airship-host-config/roles/hostconfig/tasks/main.yml delete mode 100644 airship-host-config/roles/hostconfig/vars/main.yml create mode 100644 airship-host-config/roles/setvariables/tasks/main.yml create mode 100644 airship-host-config/roles/sysctl/tasks/main.yml create mode 100644 airship-host-config/roles/ulimit/tasks/main.yml create mode 100644 docs/CR_creation_flow.png create mode 100644 docs/Deployment_Architecture.png create mode 100644 docs/Overview.md create mode 100644 docs/deployment_flow.png create mode 100644 kubernetes/haproxy.sh diff --git a/README.md b/README.md index ba70332..95f7975 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,12 @@ This repo contains the code for Airship HostConfig Application using Ansible Ope ## How to Run ## Approach 1 -If Kubernetes setup is not available please refer to README.md in kubernetes folder to bring up the kubernetes setup. It uses Vagrant and Virtual Box to bring up 1 master and 2 worker node VMs +If Kubernetes setup is not available please refer to README.md in kubernetes folder to bring up the kubernetes setup. It uses Vagrant and Virtual Box to bring up 3 master and 5 worker node VMs After the VMs are up and running, connect to master node ``` -vagrant ssh k8-master +vagrant ssh k8-master-1 ``` Navigate to airship-host-config folder @@ -24,6 +24,8 @@ Execute the create_labels.sh file so that the Kubernetes nodes are labelled acco ./create_labels.sh ``` +Please note: As part of the tasks executed whenever we are creating a Hostconfig CR object, we are checking a "hello" file in the $HOME directory of the ansible ssh user. This file is created as part of the ./setup.sh script please feel free to comment the task if not needed before builing the image. + Execute the setup.sh script to build and copy the Airship Hostconfig Ansible Operator Image to worker nodes. It also deploys the application on the Kubernetes setup as deployment kind. The below script configures Airship HostConfig Ansible Operator to use "vagrant" as both username and password when it tries connecting to the Kubernetes Nodes. So when we create a HostConfig Kubernetes CR object the application tries to execute the hostconfig ansible role on the Kubernetes Nodes specified in the CR object by connecting using the "vagrant" username and password. ``` @@ -36,6 +38,19 @@ If you want to execute the ansible playbook in the hostconfig example with a dif ./setup.sh ``` +If you are planning for the ansible-operator to use username and private key when connecting to the kubernetes node. You can use the script available that creates the private and public keys, copy the public key to kubernetes nodes, creates the secret and attach the secret as annotation. +``` +./install_ssh_private_key.sh +``` + +To try you own custom keys or custom names, follow the below commands to generate the private and public keys. Use this private key and username to generate the kuberenetes secret. Once the secret is available attach this secret name as annotation to the kubernetes node. Also copy the public key to the node. +``` +ssh-keygen -q -t rsa -N '' -f +ssh-copy-id -i @ +kubectl create secret generic --from-literal=username= --from-file=ssh_private_key= +kubectl annotate node secret= +``` + ## Approach 2 If Kubernetes setup is already available, please follow the below procedure @@ -49,7 +64,7 @@ export KUBECONFIG=~/.kube/config Clone the repository ``` -git clone https://github.com/SirishaGopigiri/airship-host-config.git +git clone https://github.com/SirishaGopigiri/airship-host-config.git -b june_29 ``` Navigate to airship-host-config folder @@ -58,6 +73,8 @@ Navigate to airship-host-config folder cd airship-host-config/airship-host-config/ ``` +Please note: As part of the tasks executed whenever we are creating a Hostconfig CR object, we are checking a "hello" file in the $HOME directory of the ansible ssh user. This file is created as part of the ./setup.sh script please feel free to comment the task if not needed before builing the image. + Execute the setup.sh script to build and copy the Airship Hostconfig Ansible Operator Image to worker nodes. It also deploys the application on the Kubernetes setup as deployment kind. The below script configures Airship HostConfig Ansible Operator to use "vagrant" as both username and password when it tries connecting to the Kubernetes Nodes. So when we create a HostConfig Kubernetes CR object the application tries to execute the hostconfig ansible role on the Kubernetes Nodes specified in the CR object by connecting using the "vagrant" username and password. ``` @@ -70,6 +87,20 @@ If you want to execute the ansible playbook in the hostconfig example with a dif ./setup.sh ``` +If you are planning for the ansible-operator to use username and private key when connecting to the kubernetes node. You can use the script available that creates the private and public keys, copy the public key to kubernetes nodes, creates the secret and attach the secret as annotation. +``` +./install_ssh_private_key.sh +``` + +To try you own custom keys or custom names, follow the below commands to generate the private and public keys. Use this private key and username to generate the kuberenetes secret. Once the secret is available attach this secret name as annotation to the kubernetes node. Also copy the public key to the node. +``` +ssh-keygen -q -t rsa -N '' -f +ssh-copy-id -i @ +kubectl create secret generic --from-literal=username= --from-file=ssh_private_key= +kubectl annotate node secret= +``` + + ## Run Examples After the setup.sh file executed successfully, navigate to demo_examples and execute the desired examples @@ -85,9 +116,9 @@ Executing examples ``` cd demo_examples -kubectl apply -f example.yaml -kubectl apply -f example1.yaml -kubectl apply -f example2.yaml +kubectl apply -f example_host_groups.yaml +kubectl apply -f example_match_host_groups.yaml +kubectl apply -f example_parallel.yaml ``` Apart from the logs on the pod when we execute the hostconfig role we are creating a "tetsing" file on the kubernetes nodes, please check the contents in that file which states the time of execution of the hostconfig role by the HostConfig Ansible Operator Pod. diff --git a/airship-host-config/README.md b/airship-host-config/README.md new file mode 100644 index 0000000..d4f1f46 --- /dev/null +++ b/airship-host-config/README.md @@ -0,0 +1,37 @@ +# Airship HostConfig Using Ansible Operator + +Here we discuss about the various variable that are used in the HostConfig CR Object to control the execution flow of the kubernetes nodes + +host_groups: Dictionary specifying the key/value labels of the Kubernetes nodes on which the playbook should be executed + +sequential: When set to true executes the host_groups labels sequentially + +match_host_groups: Performs an AND operation of the host_group labels and executes the playbook on the hosts which have all the labels matched, when set to true + +max_hosts_parallel: Caps the numbers of hosts that are executed in each iteration + +stop_on_failure: When set to true stops the playbook execution on that host and subsequent hosts whenever a task fails on a node + +max_failure_percenatge: Sets the Maximum failure percenatge of hosts that are allowed to fail on a every iteration + +reexecute: Executes the playbook again on the successful hosts as well + +ulimit, sysctl: Array objects specifiying the configuration of ulimit and sysctl on the kubernetes nodes + +The demo_examples folder has some examples listed which can be used to initially to play with the above variables + +1. example_host_groups.yaml - Gives example on how to use host_groups + +2. example_sequential.yaml - In this example the host_groups specified goes by sequence and in the first iteration the master nodes get executed and then the worker nodes get executed + +3. example_match_host_groups.yaml - In this example the playbook will be executed on all the hosts matching "us-east-1a" zone and are master nodes, "us-east-1a" and are worker nodes, "us-east-1b" and are "master" nodes, "us-east-1b" and are worker nodes. All the hosts matching the condition will be executed in parallel. + +4. example_sequential_match_host_groups.yaml - This is the same example as above but just the execution goes in sequence + +5. example_parallel.yaml - In this example we will be executing 2 hosts for every iteration + +6. example_stop_on_failure.yaml - This example shows that the execution stops whenever a task fails on any kubernetes hosts + +7. example_max_percentage.yaml - In this example the execution stops only when the hosts failing exceeds 30% at a given iteration. + +8. example_sysctl_ulimit.yaml - In this example we configure the kubernetes nodes with the values specified for ulimit and sysclt in the CR object. diff --git a/airship-host-config/build/Dockerfile b/airship-host-config/build/Dockerfile index e77f5f8..8d98f2a 100644 --- a/airship-host-config/build/Dockerfile +++ b/airship-host-config/build/Dockerfile @@ -1,17 +1,22 @@ FROM quay.io/operator-framework/ansible-operator:v0.17.0 -USER root -RUN dnf install openssh-clients -y -RUN yum install -y wget && wget http://download.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm && rpm -ivh epel-release-6-8.noarch.rpm && yum --enablerepo=epel -y install sshpass -USER ansible-operator - COPY requirements.yml ${HOME}/requirements.yml RUN ansible-galaxy collection install -r ${HOME}/requirements.yml \ && chmod -R ug+rwx ${HOME}/.ansible COPY build/ansible.cfg /etc/ansible/ansible.cfg COPY watches.yaml ${HOME}/watches.yaml +USER root +RUN dnf install openssh-clients -y +RUN yum install -y wget && wget http://download.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm && rpm -ivh epel-release-6-8.noarch.rpm && yum --enablerepo=epel -y install sshpass +USER ansible-operator + COPY roles/ ${HOME}/roles/ -COPY playbook.yaml ${HOME}/ +COPY playbooks/ ${HOME}/playbooks/ COPY inventory/ ${HOME}/inventory/ COPY plugins/ ${HOME}/plugins/ +# ansible-runner unable to pick custom callback plugins specified in any other directory other than /usr/local/lib/python3.6/site-packages/ansible/plugins/callback +# ansible-runner is overriding the ANSIBLE_CALLBACK_PLUGINS Environment variable +# https://github.com/ansible/ansible-runner/blob/stable/1.3.x/ansible_runner/runner_config.py#L178 +COPY plugins/callback/hostconfig_k8_cr_status.py /usr/local/lib/python3.6/site-packages/ansible/plugins/callback/ +RUN mkdir ${HOME}/.ssh diff --git a/airship-host-config/build/ansible.cfg b/airship-host-config/build/ansible.cfg index 4dcc9a9..8cf60f1 100644 --- a/airship-host-config/build/ansible.cfg +++ b/airship-host-config/build/ansible.cfg @@ -1,7 +1,8 @@ [defaults] inventory_plugins = /opt/ansible/plugins/inventory +callback_plugins = /opt/ansible/plugins/callback stdout_callback = yaml -callback_whitelist = profile_tasks,timer +callback_whitelist = profile_tasks,timer,hostconfig_k8_cr_status module_utils = /opt/ansible/module_utils roles_path = /opt/ansible/roles library = /opt/ansible/library diff --git a/airship-host-config/create_labels.sh b/airship-host-config/create_labels.sh index f96211a..f8fb856 100755 --- a/airship-host-config/create_labels.sh +++ b/airship-host-config/create_labels.sh @@ -1,13 +1,28 @@ #!/bin/bash -kubectl label node k8s-master kubernetes.io/role=master +kubectl label node k8s-master-1 kubernetes.io/role=master +kubectl label node k8s-master-2 kubernetes.io/role=master +kubectl label node k8s-master-3 kubernetes.io/role=master kubectl label node k8s-node-1 kubernetes.io/role=worker kubectl label node k8s-node-2 kubernetes.io/role=worker +kubectl label node k8s-node-3 kubernetes.io/role=worker +kubectl label node k8s-node-4 kubernetes.io/role=worker +kubectl label node k8s-node-5 kubernetes.io/role=worker -kubectl label node k8s-master topology.kubernetes.io/region=us-east -kubectl label node k8s-node-1 topology.kubernetes.io/region=us-west +kubectl label node k8s-master-1 topology.kubernetes.io/region=us-east +kubectl label node k8s-master-2 topology.kubernetes.io/region=us-west +kubectl label node k8s-master-3 topology.kubernetes.io/region=us-east +kubectl label node k8s-node-1 topology.kubernetes.io/region=us-east kubectl label node k8s-node-2 topology.kubernetes.io/region=us-east +kubectl label node k8s-node-3 topology.kubernetes.io/region=us-east +kubectl label node k8s-node-4 topology.kubernetes.io/region=us-west +kubectl label node k8s-node-5 topology.kubernetes.io/region=us-west -kubectl label node k8s-master topology.kubernetes.io/zone=us-east-1a -kubectl label node k8s-node-1 topology.kubernetes.io/zone=us-east-1b -kubectl label node k8s-node-2 topology.kubernetes.io/zone=us-east-1c +kubectl label node k8s-master-1 topology.kubernetes.io/zone=us-east-1a +kubectl label node k8s-master-2 topology.kubernetes.io/zone=us-west-1a +kubectl label node k8s-master-3 topology.kubernetes.io/zone=us-east-1b +kubectl label node k8s-node-1 topology.kubernetes.io/zone=us-east-1a +kubectl label node k8s-node-2 topology.kubernetes.io/zone=us-east-1a +kubectl label node k8s-node-3 topology.kubernetes.io/zone=us-east-1b +kubectl label node k8s-node-4 topology.kubernetes.io/zone=us-west-1a +kubectl label node k8s-node-5 topology.kubernetes.io/zone=us-west-1a diff --git a/airship-host-config/demo_examples/example.yaml b/airship-host-config/demo_examples/example.yaml deleted file mode 100644 index e7fc040..0000000 --- a/airship-host-config/demo_examples/example.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: hostconfig.airshipit.org/v1alpha1 -kind: HostConfig -metadata: - name: example -spec: - message: "Its a big world" diff --git a/airship-host-config/demo_examples/example3.yaml b/airship-host-config/demo_examples/example3.yaml deleted file mode 100644 index 907ec4c..0000000 --- a/airship-host-config/demo_examples/example3.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: hostconfig.airshipit.org/v1alpha1 -kind: HostConfig -metadata: - name: example3 -spec: - # Add fields here - message: "Its a big world" - host_groups: - - "us-east" - - "us-west" - execution_order: true diff --git a/airship-host-config/demo_examples/example4.yaml b/airship-host-config/demo_examples/example4.yaml deleted file mode 100644 index 5452f13..0000000 --- a/airship-host-config/demo_examples/example4.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: hostconfig.airshipit.org/v1alpha1 -kind: HostConfig -metadata: - name: example4 -spec: - # Add fields here - message: "Its a big world" - host_groups: - - "worker" - - "master" - execution_strategy: 1 - execution_order: true diff --git a/airship-host-config/demo_examples/example5.yaml b/airship-host-config/demo_examples/example5.yaml deleted file mode 100644 index f2170ee..0000000 --- a/airship-host-config/demo_examples/example5.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: hostconfig.airshipit.org/v1alpha1 -kind: HostConfig -metadata: - name: example5 -spec: - # Add fields here - message: "Its a big world" - host_groups: - - "us-east-1a" - - "us-east-1c" - - "us-east-1b" - execution_order: true diff --git a/airship-host-config/demo_examples/example1.yaml b/airship-host-config/demo_examples/example_host_groups.yaml similarity index 60% rename from airship-host-config/demo_examples/example1.yaml rename to airship-host-config/demo_examples/example_host_groups.yaml index 91a3e0a..69f3284 100644 --- a/airship-host-config/demo_examples/example1.yaml +++ b/airship-host-config/demo_examples/example_host_groups.yaml @@ -4,8 +4,7 @@ metadata: name: example1 spec: # Add fields here - message: "Its a big world" host_groups: - - "master" - - "worker" - execution_order: false + - name: "kubernetes.io/role" + values: + - "master" diff --git a/airship-host-config/demo_examples/example_match_host_groups.yaml b/airship-host-config/demo_examples/example_match_host_groups.yaml new file mode 100644 index 0000000..09f814f --- /dev/null +++ b/airship-host-config/demo_examples/example_match_host_groups.yaml @@ -0,0 +1,17 @@ +apiVersion: hostconfig.airshipit.org/v1alpha1 +kind: HostConfig +metadata: + name: example3 +spec: + # Add fields here + host_groups: + - name: "topology.kubernetes.io/zone" + values: + - "us-east-1a" + - "us-east-1b" + - name: "kubernetes.io/role" + values: + - "master" + - "worker" + sequential: false + match_host_groups: true diff --git a/airship-host-config/demo_examples/example_max_percentage.yaml b/airship-host-config/demo_examples/example_max_percentage.yaml new file mode 100644 index 0000000..f96bdf4 --- /dev/null +++ b/airship-host-config/demo_examples/example_max_percentage.yaml @@ -0,0 +1,14 @@ +apiVersion: hostconfig.airshipit.org/v1alpha1 +kind: HostConfig +metadata: + name: example5 +spec: + # Add fields here + host_groups: + - name: "kubernetes.io/role" + values: + - "master" + - "worker" + sequential: true + stop_on_failure: false + max_failure_percentage: 30 diff --git a/airship-host-config/demo_examples/example_parallel.yaml b/airship-host-config/demo_examples/example_parallel.yaml new file mode 100644 index 0000000..151b5b2 --- /dev/null +++ b/airship-host-config/demo_examples/example_parallel.yaml @@ -0,0 +1,12 @@ +apiVersion: hostconfig.airshipit.org/v1alpha1 +kind: HostConfig +metadata: + name: example7 +spec: + # Add fields here + host_groups: + - name: "kubernetes.io/role" + values: + - "master" + - "worker" + max_hosts_parallel: 2 diff --git a/airship-host-config/demo_examples/example2.yaml b/airship-host-config/demo_examples/example_sequential.yaml similarity index 57% rename from airship-host-config/demo_examples/example2.yaml rename to airship-host-config/demo_examples/example_sequential.yaml index f9b43c8..de5ca19 100644 --- a/airship-host-config/demo_examples/example2.yaml +++ b/airship-host-config/demo_examples/example_sequential.yaml @@ -4,8 +4,9 @@ metadata: name: example2 spec: # Add fields here - message: "Its a big world" host_groups: - - "master" - - "worker" - execution_order: true + - name: "kubernetes.io/role" + values: + - "master" + - "worker" + sequential: true diff --git a/airship-host-config/demo_examples/example_sequential_match_host_groups.yaml b/airship-host-config/demo_examples/example_sequential_match_host_groups.yaml new file mode 100644 index 0000000..2e276da --- /dev/null +++ b/airship-host-config/demo_examples/example_sequential_match_host_groups.yaml @@ -0,0 +1,17 @@ +apiVersion: hostconfig.airshipit.org/v1alpha1 +kind: HostConfig +metadata: + name: example4 +spec: + # Add fields here + host_groups: + - name: "topology.kubernetes.io/zone" + values: + - "us-east-1a" + - "us-east-1b" + - name: "kubernetes.io/role" + values: + - "master" + - "worker" + sequential: true + match_host_groups: true diff --git a/airship-host-config/demo_examples/example_stop_on_failure.yaml b/airship-host-config/demo_examples/example_stop_on_failure.yaml new file mode 100644 index 0000000..1b99973 --- /dev/null +++ b/airship-host-config/demo_examples/example_stop_on_failure.yaml @@ -0,0 +1,13 @@ +apiVersion: hostconfig.airshipit.org/v1alpha1 +kind: HostConfig +metadata: + name: example6 +spec: + # Add fields here + host_groups: + - name: "kubernetes.io/role" + values: + - "master" + - "worker" + sequential: true + stop_on_failure: true diff --git a/airship-host-config/demo_examples/example_sysctl_ulimit.yaml b/airship-host-config/demo_examples/example_sysctl_ulimit.yaml new file mode 100644 index 0000000..d8f046d --- /dev/null +++ b/airship-host-config/demo_examples/example_sysctl_ulimit.yaml @@ -0,0 +1,23 @@ +apiVersion: hostconfig.airshipit.org/v1alpha1 +kind: HostConfig +metadata: + name: example8 +spec: + # Add fields here + host_groups: + - name: "kubernetes.io/role" + values: + - "master" + sequential: false + reexecute: false + config: + sysctl: + - name: "net.ipv6.route.gc_interval" + value: "30" + - name: "net.netfilter.nf_conntrack_frag6_timeout" + value: "120" + ulimit: + - user: "sirisha" + type: "hard" + item: "cpu" + value: "unlimited" diff --git a/airship-host-config/deploy/cluster_role_binding.yaml b/airship-host-config/deploy/cluster_role_binding.yaml index 053b886..0b1fd7d 100644 --- a/airship-host-config/deploy/cluster_role_binding.yaml +++ b/airship-host-config/deploy/cluster_role_binding.yaml @@ -9,4 +9,4 @@ roleRef: subjects: - kind: ServiceAccount name: airship-host-config - namespace: default \ No newline at end of file + namespace: default diff --git a/airship-host-config/deploy/crds/hostconfig.airshipit.org_hostconfigs_crd.yaml b/airship-host-config/deploy/crds/hostconfig.airshipit.org_hostconfigs_crd.yaml index 3b6e859..94c33c8 100644 --- a/airship-host-config/deploy/crds/hostconfig.airshipit.org_hostconfigs_crd.yaml +++ b/airship-host-config/deploy/crds/hostconfig.airshipit.org_hostconfigs_crd.yaml @@ -9,6 +9,8 @@ spec: listKind: HostConfigList plural: hostconfigs singular: hostconfig + shortNames: + - hc scope: Namespaced subresources: status: {} @@ -16,7 +18,84 @@ spec: openAPIV3Schema: type: object x-kubernetes-preserve-unknown-fields: true + properties: + spec: + description: "HostConfig Spec to perform hostconfig Opertaions." + type: object + properties: + host_groups: + description: "Array of host_groups to select hosts on which to perform host configuration." + type: array + items: + type: object + description: "Node labels to be given as key value pairs. Values can be given as list." + properties: + name: + type: string + description: "Node label key values for host selection." + values: + type: array + description: "Node label values for host selection." + items: + type: string + required: + - name + - values + match_host_groups: + type: boolean + description: "Set to true to perform an AND opertion of all the host_groups specified." + sequential: + type: boolean + description: "Set to true if the host_groups execution needs to happen in sequence." + reexecute: + type: boolean + description: "Set to true if execution needs to happen on the success nodes as well. Is applicable only when atleast one of the node fails. The execution repeats for all the nodes." + stop_on_failure: + type: boolean + description: "Set to true if any one node configuration fails, to stop the execution of the other nodes as well." + max_hosts_parallel: + type: integer + description: "Set to integer number, stating max how many hosts can execute at the same time." + max_failure_percentage: + type: integer + description: "Set the integer percentage value, to state how much max percentage of hosts can fail for every iteration before stoping the execution." + config: + type: object + description: "The configuration details that needs to be performed on the targeted kubernetes nodes." + properties: + ulimit: + description: "An array of ulimit configuration to be performed on the target nodes." + type: array + items: + type: object + properties: + user: + type: string + type: + type: string + item: + type: string + value: + type: string + required: + - user + - value + - type + - item + sysctl: + description: "An array of sysctl configuration to be performed on the target nodes." + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + required: + - name + - value versions: - - name: v1alpha1 - served: true - storage: true + - name: v1alpha1 + served: true + storage: true diff --git a/airship-host-config/deploy/crds/hostconfig.airshipit.org_v1alpha1_hostconfig_cr.yaml b/airship-host-config/deploy/crds/hostconfig.airshipit.org_v1alpha1_hostconfig_cr.yaml deleted file mode 100644 index a92781e..0000000 --- a/airship-host-config/deploy/crds/hostconfig.airshipit.org_v1alpha1_hostconfig_cr.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: hostconfig.airshipit.org/v1alpha1 -kind: HostConfig -metadata: - name: example-hostconfig -spec: - # Add fields here - message: "Its a big world" diff --git a/airship-host-config/deploy/operator.yaml b/airship-host-config/deploy/operator.yaml index f5401de..2e81757 100644 --- a/airship-host-config/deploy/operator.yaml +++ b/airship-host-config/deploy/operator.yaml @@ -17,13 +17,13 @@ spec: containers: - name: airship-host-config # Replace this with the built image name - image: "AIRSHIP_HOSTCONFIG_IMAGE" - imagePullPolicy: "PULL_POLICY" - securityContext: - privileged: true + image: "quay.io/sirishagopigiri/airship-host-config" + imagePullPolicy: "IfNotPresent" volumeMounts: - mountPath: /tmp/ansible-operator/runner name: runner + - mountPath: /opt/ansible/data + name: data env: - name: WATCH_NAMESPACE valueFrom: @@ -35,6 +35,10 @@ spec: fieldPath: metadata.name - name: OPERATOR_NAME value: "airship-host-config" + - name: ANSIBLE_FILTER_PLUGINS + value: /opt/ansible/plugins/filter + - name: ANSIBLE_FORKS + value: "100" - name: ANSIBLE_GATHERING value: explicit - name: ANSIBLE_INVENTORY @@ -43,6 +47,10 @@ spec: value: "USERNAME" - name: PASS value: "PASSWORD" + - name: SECRET_NAMESPACE + value: "default" volumes: - name: runner emptyDir: {} + - name: data + emptyDir: {} diff --git a/airship-host-config/deploy/role.yaml b/airship-host-config/deploy/role.yaml index ec00911..a197bf3 100644 --- a/airship-host-config/deploy/role.yaml +++ b/airship-host-config/deploy/role.yaml @@ -8,6 +8,8 @@ rules: - "" resources: - pods + - pods/exec + - pods/log - services - services/finalizers - endpoints @@ -70,6 +72,7 @@ rules: - hostconfig.airshipit.org resources: - '*' + - inventories verbs: - create - delete diff --git a/airship-host-config/install_ssh_private_key.sh b/airship-host-config/install_ssh_private_key.sh new file mode 100755 index 0000000..703563a --- /dev/null +++ b/airship-host-config/install_ssh_private_key.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +hosts=(`kubectl get nodes -o wide | awk '{print $1}' | sed -e '1d'`) +hosts_ips=(`kubectl get nodes -o wide | awk '{print $6}' | sed -e '1d'`) + +get_username_password(){ + if [ -z "$1" ]; then USERNAME="vagrant"; else USERNAME=$1; fi + if [ -z "$2" ]; then PASSWORD="vagrant"; else PASSWORD=$2; fi + echo $USERNAME $PASSWORD +} + +copy_ssh_keys(){ + read USERNAME PASSWORD < <(get_username_password $1 $2) + for i in "${!hosts[@]}" + do + printf 'Working on host %s with Index %s and having IP %s\n' "${hosts[i]}" "$i" "${hosts_ips[i]}" + ssh-keygen -q -t rsa -N '' -f ${hosts[i]} + sshpass -p $PASSWORD ssh-copy-id -o StrictHostKeyChecking=no -i ${hosts[i]} $USERNAME@${hosts_ips[i]} + kubectl create secret generic ${hosts[i]} --from-literal=username=$USERNAME --from-file=ssh_private_key=${hosts[i]} + kubectl annotate node ${hosts[i]} secret=${hosts[i]} + done +} + +copy_ssh_keys $1 $2 diff --git a/airship-host-config/inventory/dynamic_inventory.py b/airship-host-config/inventory/dynamic_inventory.py index e47d8cc..5ed571b 100755 --- a/airship-host-config/inventory/dynamic_inventory.py +++ b/airship-host-config/inventory/dynamic_inventory.py @@ -4,6 +4,7 @@ import os import sys import argparse import time +import base64 import kubernetes.client from kubernetes.client.rest import ApiException import yaml @@ -33,25 +34,62 @@ class KubeInventory(object): # Kube driven inventory def kube_inventory(self): self.inventory = {"group": {"hosts": [], "vars": {}}, "_meta": {"hostvars": {}}} - self.set_ssh_keys() self.get_nodes() - # Sets the ssh username and password using the pod environment variables - def set_ssh_keys(self): - self.inventory["group"]["vars"]["ansible_ssh_user"] = os.environ.get("USER") if "USER" in os.environ else "kubernetes" - if "PASS" in os.environ: - self.inventory["group"]["vars"]["ansible_ssh_pass"] = os.environ.get("PASS") + # Sets the ssh username and password using the secret name given in the label + def _set_ssh_keys(self, labels, node_internalip, node_name): + namespace = "" + if "SECRET_NAMESPACE" in os.environ: + namespace = os.environ.get("SECRET_NAMESPACE") else: - self.inventory["group"]["vars"][ - "ansible_ssh_private_key_file" - ] = "~/.ssh/id_rsa" + namespace = "default" + if "secret" in labels.keys(): + try: + secret_value = self.api_instance.read_namespaced_secret(labels["secret"], namespace) + except ApiException as e: + return False + if "username" in secret_value.data.keys(): + username = (base64.b64decode(secret_value.data['username'])).decode("utf-8") + self.inventory["_meta"]["hostvars"][node_internalip]["ansible_ssh_user"] = username + elif "USER" in os.environ: + self.inventory["_meta"]["hostvars"][node_internalip]["ansible_ssh_user"] = os.environ.get("USER") + else: + self.inventory["_meta"]["hostvars"][node_internalip]["ansible_ssh_user"] = 'kubernetes' + if "password" in secret_value.data.keys(): + password = (base64.b64decode(secret_value.data['password'])).decode("utf-8") + self.inventory["_meta"]["hostvars"][node_internalip]["ansible_ssh_pass"] = password + elif "ssh_private_key" in secret_value.data.keys(): + private_key = (base64.b64decode(secret_value.data['ssh_private_key'])).decode("utf-8") + fileName = "/opt/ansible/.ssh/"+node_name + with open(os.open(fileName, os.O_CREAT | os.O_WRONLY, 0o644), 'w') as f: + f.write(private_key) + f.close() + os.chmod(fileName, 0o600) + self.inventory["_meta"]["hostvars"][node_internalip][ + "ansible_ssh_private_key_file"] = fileName + elif "PASS" in os.environ: + self.inventory["_meta"]["hostvars"][node_internalip]["ansible_ssh_pass"] = os.environ.get("PASS") + else: + self.inventory["_meta"]["hostvars"][node_internalip]["ansible_ssh_pass"] = 'kubernetes' + else: + return False + return True + + # Sets default username and password from environment variables or some default username/password + def _set_default_ssh_keys(self, node_internalip): + if "USER" in os.environ: + self.inventory["_meta"]["hostvars"][node_internalip]["ansible_ssh_user"] = os.environ.get("USER") + else: + self.inventory["_meta"]["hostvars"][node_internalip]["ansible_ssh_user"] = 'kubernetes' + if "PASS" in os.environ: + self.inventory["_meta"]["hostvars"][node_internalip]["ansible_ssh_pass"] = os.environ.get("PASS") + else: + self.inventory["_meta"]["hostvars"][node_internalip]["ansible_ssh_pass"] = 'kubernetes' return # Gets the Kubernetes nodes labels and annotations and build the inventory # Also groups the kubernetes nodes based on the labels and annotations def get_nodes(self): - #label_selector = "kubernetes.io/role="+role - try: nodes = self.api_instance.list_node().to_dict()[ "items" @@ -70,25 +108,31 @@ class KubeInventory(object): self.inventory["group"]["hosts"].append(node_internalip) self.inventory["_meta"]["hostvars"][node_internalip] = {} + node_name = node["metadata"]["name"] + self.inventory["_meta"]["hostvars"][node_internalip][ + "kube_node_name"] = node_name + if not self._set_ssh_keys(node["metadata"]["annotations"], node_internalip, node_name): + self._set_default_ssh_keys(node_internalip) + # As the annotations are not of interest so not adding them to ansible host groups + # Only updating the host variable with annotations for key, value in node["metadata"]["annotations"].items(): self.inventory["_meta"]["hostvars"][node_internalip][key] = value + # Add groups based on labels and also updates the host variables for key, value in node["metadata"]["labels"].items(): self.inventory["_meta"]["hostvars"][node_internalip][key] = value if key in interested_labels_annotations: - if value not in self.inventory.keys(): - self.inventory[value] = {"hosts": [], "vars": {}} - if node_internalip not in self.inventory[value]["hosts"]: - self.inventory[value]["hosts"].append(node_internalip) + if key+'_'+value not in self.inventory.keys(): + self.inventory[key+'_'+value] = {"hosts": [], "vars": {}} + if node_internalip not in self.inventory[key+'_'+value]["hosts"]: + self.inventory[key+'_'+value]["hosts"].append(node_internalip) + # Add groups based on node info and also updates the host variables for key, value in node['status']['node_info'].items(): self.inventory["_meta"]["hostvars"][node_internalip][key] = value if key in interested_labels_annotations: - if value not in self.inventory.keys(): - self.inventory[value] = {"hosts": [], "vars": {}} - if node_internalip not in self.inventory[value]["hosts"]: - self.inventory[value]["hosts"].append(node_internalip) - self.inventory["_meta"]["hostvars"][node_internalip][ - "kube_node_name" - ] = node["metadata"]["name"] + if key+'_'+value not in self.inventory.keys(): + self.inventory[key+'_'+value] = {"hosts": [], "vars": {}} + if node_internalip not in self.inventory[key+'_'+value]["hosts"]: + self.inventory[key+'_'+value]["hosts"].append(node_internalip) return def empty_inventory(self): diff --git a/airship-host-config/molecule/cluster/converge.yml b/airship-host-config/molecule/cluster/converge.yml deleted file mode 100644 index 8877f82..0000000 --- a/airship-host-config/molecule/cluster/converge.yml +++ /dev/null @@ -1,24 +0,0 @@ ---- -- name: Converge - hosts: localhost - connection: local - gather_facts: no - collections: - - community.kubernetes - - tasks: - - name: Ensure operator image is set - fail: - msg: | - You must specify the OPERATOR_IMAGE environment variable in order to run the - 'cluster' scenario - when: not operator_image - - - name: Create the Operator Deployment - k8s: - namespace: '{{ namespace }}' - definition: "{{ lookup('template', '/'.join([template_dir, 'operator.yaml.j2'])) }}" - wait: yes - vars: - image: '{{ operator_image }}' - pull_policy: '{{ operator_pull_policy }}' diff --git a/airship-host-config/molecule/cluster/create.yml b/airship-host-config/molecule/cluster/create.yml deleted file mode 100644 index 1eeaf92..0000000 --- a/airship-host-config/molecule/cluster/create.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -- name: Create - hosts: localhost - connection: local - gather_facts: false - tasks: [] diff --git a/airship-host-config/molecule/cluster/destroy.yml b/airship-host-config/molecule/cluster/destroy.yml deleted file mode 100644 index fac2af8..0000000 --- a/airship-host-config/molecule/cluster/destroy.yml +++ /dev/null @@ -1,34 +0,0 @@ ---- -- name: Destroy - hosts: localhost - connection: local - gather_facts: false - no_log: "{{ molecule_no_log }}" - collections: - - community.kubernetes - - tasks: - - name: Delete namespace - k8s: - api_version: v1 - kind: Namespace - name: '{{ namespace }}' - state: absent - wait: yes - - - name: Delete RBAC resources - k8s: - definition: "{{ lookup('template', '/'.join([deploy_dir, item])) }}" - namespace: '{{ namespace }}' - state: absent - wait: yes - with_items: - - role.yaml - - role_binding.yaml - - service_account.yaml - - - name: Delete Custom Resource Definition - k8s: - definition: "{{ lookup('file', '/'.join([deploy_dir, 'crds/hostconfig.airshipit.org_hostconfigs_crd.yaml'])) }}" - state: absent - wait: yes diff --git a/airship-host-config/molecule/cluster/molecule.yml b/airship-host-config/molecule/cluster/molecule.yml deleted file mode 100644 index 06b307f..0000000 --- a/airship-host-config/molecule/cluster/molecule.yml +++ /dev/null @@ -1,35 +0,0 @@ ---- -dependency: - name: galaxy -driver: - name: delegated -lint: | - set -e - yamllint -d "{extends: relaxed, rules: {line-length: {max: 120}}}" . -platforms: -- name: cluster - groups: - - k8s -provisioner: - name: ansible - lint: | - set -e - ansible-lint - inventory: - group_vars: - all: - namespace: ${TEST_OPERATOR_NAMESPACE:-osdk-test} - host_vars: - localhost: - ansible_python_interpreter: '{{ ansible_playbook_python }}' - deploy_dir: ${MOLECULE_PROJECT_DIRECTORY}/deploy - template_dir: ${MOLECULE_PROJECT_DIRECTORY}/molecule/templates - operator_image: ${OPERATOR_IMAGE:-""} - operator_pull_policy: ${OPERATOR_PULL_POLICY:-"Always"} - env: - K8S_AUTH_KUBECONFIG: ${KUBECONFIG:-"~/.kube/config"} -verifier: - name: ansible - lint: | - set -e - ansible-lint diff --git a/airship-host-config/molecule/cluster/prepare.yml b/airship-host-config/molecule/cluster/prepare.yml deleted file mode 100644 index ad8062e..0000000 --- a/airship-host-config/molecule/cluster/prepare.yml +++ /dev/null @@ -1,31 +0,0 @@ ---- -- name: Prepare - hosts: localhost - connection: local - gather_facts: false - no_log: "{{ molecule_no_log }}" - collections: - - community.kubernetes - - vars: - deploy_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/deploy" - - tasks: - - name: Create Custom Resource Definition - k8s: - definition: "{{ lookup('file', '/'.join([deploy_dir, 'crds/hostconfig.airshipit.org_hostconfigs_crd.yaml'])) }}" - - - name: Create namespace - k8s: - api_version: v1 - kind: Namespace - name: '{{ namespace }}' - - - name: Create RBAC resources - k8s: - definition: "{{ lookup('template', '/'.join([deploy_dir, item])) }}" - namespace: '{{ namespace }}' - with_items: - - role.yaml - - role_binding.yaml - - service_account.yaml diff --git a/airship-host-config/molecule/cluster/verify.yml b/airship-host-config/molecule/cluster/verify.yml deleted file mode 100644 index 2a11f4b..0000000 --- a/airship-host-config/molecule/cluster/verify.yml +++ /dev/null @@ -1,35 +0,0 @@ ---- -# This is an example playbook to execute Ansible tests. -- name: Verify - hosts: localhost - connection: local - gather_facts: no - collections: - - community.kubernetes - - vars: - custom_resource: "{{ lookup('template', '/'.join([deploy_dir, 'crds/hostconfig.airshipit.org_v1alpha1_hostconfig_cr.yaml'])) | from_yaml }}" - - tasks: - - name: Create the hostconfig.airshipit.org/v1alpha1.HostConfig and wait for reconciliation to complete - k8s: - state: present - namespace: '{{ namespace }}' - definition: '{{ custom_resource }}' - wait: yes - wait_timeout: 300 - wait_condition: - type: Running - reason: Successful - status: "True" - - - name: Get Pods - k8s_info: - api_version: v1 - kind: Pod - namespace: '{{ namespace }}' - register: pods - - - name: Example assertion - assert: - that: (pods | length) > 0 diff --git a/airship-host-config/molecule/default/converge.yml b/airship-host-config/molecule/default/converge.yml deleted file mode 100644 index 842b821..0000000 --- a/airship-host-config/molecule/default/converge.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -- name: Converge - hosts: localhost - connection: local - roles: - - hostconfig diff --git a/airship-host-config/molecule/default/molecule.yml b/airship-host-config/molecule/default/molecule.yml deleted file mode 100644 index dbb6800..0000000 --- a/airship-host-config/molecule/default/molecule.yml +++ /dev/null @@ -1,45 +0,0 @@ ---- -dependency: - name: galaxy -driver: - name: docker -lint: | - set -e - yamllint -d "{extends: relaxed, rules: {line-length: {max: 120}}}" . -platforms: -- name: kind-default - groups: - - k8s - image: bsycorp/kind:latest-${KUBE_VERSION:-1.17} - privileged: True - override_command: no - exposed_ports: - - 8443/tcp - - 10080/tcp - published_ports: - - 0.0.0.0:${TEST_CLUSTER_PORT:-9443}:8443/tcp - pre_build_image: yes -provisioner: - name: ansible - log: True - lint: | - set -e - ansible-lint - inventory: - group_vars: - all: - namespace: ${TEST_OPERATOR_NAMESPACE:-osdk-test} - kubeconfig_file: ${MOLECULE_EPHEMERAL_DIRECTORY}/kubeconfig - host_vars: - localhost: - ansible_python_interpreter: '{{ ansible_playbook_python }}' - env: - K8S_AUTH_KUBECONFIG: ${MOLECULE_EPHEMERAL_DIRECTORY}/kubeconfig - KUBECONFIG: ${MOLECULE_EPHEMERAL_DIRECTORY}/kubeconfig - ANSIBLE_ROLES_PATH: ${MOLECULE_PROJECT_DIRECTORY}/roles - KIND_PORT: '${TEST_CLUSTER_PORT:-9443}' -verifier: - name: ansible - lint: | - set -e - ansible-lint diff --git a/airship-host-config/molecule/default/prepare.yml b/airship-host-config/molecule/default/prepare.yml deleted file mode 100644 index 0ef0907..0000000 --- a/airship-host-config/molecule/default/prepare.yml +++ /dev/null @@ -1,27 +0,0 @@ ---- -- name: Prepare - hosts: k8s - gather_facts: no - tasks: - - name: Fetch the kubeconfig - fetch: - dest: '{{ kubeconfig_file }}' - flat: yes - src: /root/.kube/config - - - name: Change the kubeconfig port to the proper value - replace: - regexp: '8443' - replace: "{{ lookup('env', 'KIND_PORT') }}" - path: '{{ kubeconfig_file }}' - delegate_to: localhost - - - name: Wait for the Kubernetes API to become available (this could take a minute) - uri: - url: "http://localhost:10080/kubernetes-ready" - status_code: 200 - validate_certs: no - register: result - until: (result.status|default(-1)) == 200 - retries: 60 - delay: 5 diff --git a/airship-host-config/molecule/default/verify.yml b/airship-host-config/molecule/default/verify.yml deleted file mode 100644 index cf5b254..0000000 --- a/airship-host-config/molecule/default/verify.yml +++ /dev/null @@ -1,18 +0,0 @@ ---- -- name: Verify - hosts: localhost - connection: local - tasks: - - name: Get all pods in {{ namespace }} - k8s_info: - api_version: v1 - kind: Pod - namespace: '{{ namespace }}' - register: pods - - - name: Output pods - debug: var=pods - - - name: Example assertion - assert: - that: true diff --git a/airship-host-config/molecule/templates/operator.yaml.j2 b/airship-host-config/molecule/templates/operator.yaml.j2 deleted file mode 100644 index dbe248d..0000000 --- a/airship-host-config/molecule/templates/operator.yaml.j2 +++ /dev/null @@ -1,40 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: airship-host-config -spec: - replicas: 1 - selector: - matchLabels: - name: airship-host-config - template: - metadata: - labels: - name: airship-host-config - spec: - serviceAccountName: airship-host-config - containers: - - name: airship-host-config - # Replace this with the built image name - image: "{{ image }}" - imagePullPolicy: "{{ pull_policy }}" - volumeMounts: - - mountPath: /tmp/ansible-operator/runner - name: runner - env: - - name: WATCH_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: OPERATOR_NAME - value: "airship-host-config" - - name: ANSIBLE_GATHERING - value: explicit - volumes: - - name: runner - emptyDir: {} diff --git a/airship-host-config/molecule/test-local/converge.yml b/airship-host-config/molecule/test-local/converge.yml deleted file mode 100644 index 2e1d562..0000000 --- a/airship-host-config/molecule/test-local/converge.yml +++ /dev/null @@ -1,42 +0,0 @@ ---- -- name: Build Operator in Kubernetes docker container - hosts: k8s - collections: - - community.kubernetes - - vars: - image: hostconfig.airshipit.org/airship-host-config:testing - - tasks: - # using command so we don't need to install any dependencies - - name: Get existing image hash - command: docker images -q {{ image }} - register: prev_hash_raw - changed_when: false - - - name: Build Operator Image - command: docker build -f /build/build/Dockerfile -t {{ image }} /build - register: build_cmd - changed_when: not hash or (hash and hash not in cmd_out) - vars: - hash: '{{ prev_hash_raw.stdout }}' - cmd_out: '{{ "".join(build_cmd.stdout_lines[-2:]) }}' - -- name: Converge - hosts: localhost - connection: local - collections: - - community.kubernetes - - vars: - image: hostconfig.airshipit.org/airship-host-config:testing - operator_template: "{{ '/'.join([template_dir, 'operator.yaml.j2']) }}" - - tasks: - - name: Create the Operator Deployment - k8s: - namespace: '{{ namespace }}' - definition: "{{ lookup('template', operator_template) }}" - wait: yes - vars: - pull_policy: Never diff --git a/airship-host-config/molecule/test-local/molecule.yml b/airship-host-config/molecule/test-local/molecule.yml deleted file mode 100644 index f723cb7..0000000 --- a/airship-host-config/molecule/test-local/molecule.yml +++ /dev/null @@ -1,47 +0,0 @@ ---- -dependency: - name: galaxy -driver: - name: docker -lint: | - set -e - yamllint -d "{extends: relaxed, rules: {line-length: {max: 120}}}" . -platforms: - - name: kind-test-local - groups: - - k8s - image: bsycorp/kind:latest-${KUBE_VERSION:-1.17} - privileged: true - override_command: false - exposed_ports: - - 8443/tcp - - 10080/tcp - published_ports: - - 0.0.0.0:${TEST_CLUSTER_PORT:-10443}:8443/tcp - pre_build_image: true - volumes: - - ${MOLECULE_PROJECT_DIRECTORY}:/build:Z -provisioner: - name: ansible - log: true - lint: - name: ansible-lint - inventory: - group_vars: - all: - namespace: ${TEST_OPERATOR_NAMESPACE:-osdk-test} - kubeconfig_file: ${MOLECULE_EPHEMERAL_DIRECTORY}/kubeconfig - host_vars: - localhost: - ansible_python_interpreter: '{{ ansible_playbook_python }}' - template_dir: ${MOLECULE_PROJECT_DIRECTORY}/molecule/templates - deploy_dir: ${MOLECULE_PROJECT_DIRECTORY}/deploy - env: - K8S_AUTH_KUBECONFIG: ${MOLECULE_EPHEMERAL_DIRECTORY}/kubeconfig - KUBECONFIG: ${MOLECULE_EPHEMERAL_DIRECTORY}/kubeconfig - ANSIBLE_ROLES_PATH: ${MOLECULE_PROJECT_DIRECTORY}/roles - KIND_PORT: '${TEST_CLUSTER_PORT:-10443}' -verifier: - name: ansible - lint: - name: ansible-lint diff --git a/airship-host-config/molecule/test-local/prepare.yml b/airship-host-config/molecule/test-local/prepare.yml deleted file mode 100644 index 6d57ace..0000000 --- a/airship-host-config/molecule/test-local/prepare.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -- import_playbook: ../default/prepare.yml -- import_playbook: ../cluster/prepare.yml diff --git a/airship-host-config/molecule/test-local/verify.yml b/airship-host-config/molecule/test-local/verify.yml deleted file mode 100644 index 4c00308..0000000 --- a/airship-host-config/molecule/test-local/verify.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -- import_playbook: ../cluster/verify.yml diff --git a/airship-host-config/playbook.yaml b/airship-host-config/playbook.yaml deleted file mode 100644 index 0eb3272..0000000 --- a/airship-host-config/playbook.yaml +++ /dev/null @@ -1,62 +0,0 @@ ---- -#playbook.yaml - -# Ansible play to initialize custom variables -# The below block of sequence executes only when the execution order is set to true -# Which tells the ansible to execute the host_groups in sequential -- name: DISPLAY THE INVENTORY VARS - hosts: localhost - gather_facts: no - tasks: - - name: Set Serial variable - block: - ## Calculates the serial variable based on the host_groups defined in the Kubernetes Hostconfig CR object - ## Uses the custom host_config_serial plugin and returns a list of integers - ## These integer values corresponds to the number of hosts in each host group given un Kubernetes Hostconfig CR object - ## If we have 3 master and 5 worker nodes setup. And in the Kubernetes Hostconfig CR object we pass the - ## host_groups as master and worker, then using the host_config_serial plugin the variable returned - ## would be list of 3, 5 i.e [3, 5] so that all the 3 master execute in first iteration and - ## next the 5 workers execute in second iteration - ## This takes the groups parameters set by the dynamic_inventory.py as argument - - set_fact: - host_config_serial_variable: "{{ host_groups|host_config_serial(groups) }}" - ## This custom filter plugin is used to futher break the host_config_serial variable into equal length - ## as specified in the Kubernetes Hostconfig CR object - ## If we have 3 master and 5 worker nodes setup. And in the Kubernetes Hostconfig CR object we pass the - ## host_groups as master and worker, also the serial_strategy is set to 2. Then this custom filter returns - ## the following list of integers where the [3, 5] list is further split based on the - ## serial_strategy given here it is 2 - ## host_config_serial_variable is [2, 1, 2, 2, 1] - ## This task is executed only when the execution_strategy is defined in the hostconfig CR object - ## When executed it overrides the previous task value for host_config_serial_variable variable - ## This takes host_groups and groups as parameters - - set_fact: - host_config_serial_variable: "{{ execution_strategy|host_config_serial_strategy(host_groups, groups) }}" - when: execution_strategy is defined - - debug: - msg: "Serial Variable {{ host_config_serial_variable }}" - when: execution_order is true and host_groups is defined - -# The tasks below gets executed when execution_order is set to true and order of execution should be -# considered while executing -# The below tasks considers the host_config_serial_variable variable value set from the previous block -# Executes the number of hosts set in the host_config_serial_variable at every iteration -- name: Execute Roles based on hosts - hosts: "{{ host_groups | default('all')}}" - serial: "{{ hostvars['localhost']['host_config_serial_variable'] | default('100%') }}" - gather_facts: no - tasks: - - import_role: - name: hostconfig - when: execution_order is true - -# Executed when the execution_order is set to false or not set -# This is the default execution flow where ansible gets all the host available in host_groups -# and executes them in parallel -- name: Execute Roles based on hosts - hosts: "{{ host_groups | default('all')}}" - gather_facts: no - tasks: - - import_role: - name: hostconfig - when: execution_order is undefined or execution_order is false diff --git a/airship-host-config/playbooks/create_playbook.yaml b/airship-host-config/playbooks/create_playbook.yaml new file mode 100644 index 0000000..3bebd30 --- /dev/null +++ b/airship-host-config/playbooks/create_playbook.yaml @@ -0,0 +1,87 @@ +--- +#playbook.yaml + +# Ansible play to initialize custom variables +# The below blocks of helps in setting the ansible variables +# according to the CR object passed +- name: DISPLAY THE INVENTORY VARS + collections: + - community.kubernetes + - operator_sdk.util + hosts: localhost + gather_facts: no + tasks: + - name: Set Local Variables + block: + - import_role: + name: setvariables + +# The play gets executed when the stop_on_failure is undefined or set to false +# stating that the play book execution shouldn't stop even if the tasks fail on the hosts +# The below tasks considers the host_config_serial_variable variable value set from the previous block +# Executes the number of hosts set in the host_config_serial_variable at every iteration +- name: Execute Roles based on hosts and based on the Failure condition + collections: + - community.kubernetes + - operator_sdk.util + hosts: "{{ hostvars['localhost']['hostconfig_host_groups'] | default('all')}}" + serial: "{{ hostvars['localhost']['hostconfig_serial_variable'] | default('100%') }}" + any_errors_fatal: "{{ stop_on_failure|default(false) }}" + gather_facts: no + tasks: + - name: HostConfig Block + block: + - import_role: + name: sysctl + when: config.sysctl is defined + - import_role: + name: ulimit + when: config.ulimit is defined + - name: Update the file for success hosts + local_action: lineinfile line={{ inventory_hostname }} create=yes dest=/opt/ansible/data/hostconfig/{{ meta.name }}/success_hosts + throttle: 1 + rescue: + - name: Update the file for Failed hosts + local_action: lineinfile line={{ inventory_hostname }} create=yes dest=/opt/ansible/data/hostconfig/{{ meta.name }}/failed_hosts + throttle: 1 + when: ((stop_on_failure is undefined or stop_on_failure is defined) and max_failure_percentage is undefined) or (stop_on_failure is true and max_failure_percentage is defined) + +# The below play executes with hostconfig role only when the stop_failure is false +# and when the max_failure_percentage variable is defined. +# The below tasks considers the host_config_serial_variable variable value set from the previous block +# Executes the number of hosts set in the host_config_serial_variable at every iteration +- name: Execute Roles based on hosts and based on percentage of Failure + hosts: "{{ hostvars['localhost']['hostconfig_host_groups'] | default('all')}}" + serial: "{{ hostvars['localhost']['hostconfig_serial_variable'] | default('100%') }}" + max_fail_percentage: "{{ hostvars['localhost']['max_failure_percentage'] }}" + gather_facts: no + tasks: + - name: Max Percetage Block + block: + - import_role: + name: sysctl + when: config.sysctl is defined + - import_role: + name: ulimit + when: config.ulimit is defined + when: (stop_on_failure is false or stop_on_failure is undefined) and (max_failure_percentage is defined) + +# Update K8 CR Status +- name: Update CR Status + collections: + - community.kubernetes + - operator_sdk.util + hosts: localhost + gather_facts: no + tasks: + - name: Update CR Status + block: + - name: Write results to resource status + k8s_status: + api_version: hostconfig.airshipit.org/v1alpha1 + kind: HostConfig + name: '{{ meta.name }}' + namespace: '{{ meta.namespace }}' + status: + hostConfigStatus: "{{ hostConfigStatus }}" + when: hostConfigStatus is defined diff --git a/airship-host-config/playbooks/delete_playbook.yaml b/airship-host-config/playbooks/delete_playbook.yaml new file mode 100644 index 0000000..ccbd6ac --- /dev/null +++ b/airship-host-config/playbooks/delete_playbook.yaml @@ -0,0 +1,10 @@ +--- +- name: Delete LocalHosts + hosts: localhost + gather_facts: no + tasks: + - name: delete the files + file: + path: "/opt/ansible/data/hostconfig/{{ meta.name }}" + state: absent + register: output diff --git a/airship-host-config/plugins/callback/hostconfig_k8_cr_status.py b/airship-host-config/plugins/callback/hostconfig_k8_cr_status.py new file mode 100644 index 0000000..703a723 --- /dev/null +++ b/airship-host-config/plugins/callback/hostconfig_k8_cr_status.py @@ -0,0 +1,112 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' + callback: hostconfig_k8_cr_status + callback_type: aggregate + requirements: + - whitelist in configuration + short_description: Adds time to play stats + version_added: "2.0" + description: + - This callback just adds total play duration to the play stats. +''' + +from ansible.plugins.callback import CallbackBase + + +class CallbackModule(CallbackBase): + """ + This callback module tells you how long your plays ran for. + """ + CALLBACK_VERSION = 2.0 + CALLBACK_TYPE = 'aggregate' + CALLBACK_NAME = 'hostconfig_k8_cr_status' + CALLBACK_NEEDS_WHITELIST = True + + def __init__(self): + super(CallbackModule, self).__init__() + + def v2_playbook_on_play_start(self, play): + self.vm = play.get_variable_manager() + self.skip_status_tasks = ["debug", "k8s_status", "local_action", "set_fact", "k8s_info", "lineinfile"] + + def runner_on_failed(self, host, result, ignore_errors=False): + self.v2_runner_on_failed(result, ignore_errors=False) + + def runner_on_ok(self, host, res): + self.v2_runner_on_ok(result) + + def v2_runner_on_failed(self, result, ignore_errors=False): + self.set_host_config_status(result, True) + return + + def v2_runner_on_ok(self, result): + hostname = result._host.name + if result._task_fields["action"] in self.skip_status_tasks: + return + self.set_host_config_status(result) + return + + def set_host_config_status(self, result, failed=False): + hostname = result._host.name + task_name = result.task_name + task_result = result._result + status = dict() + hostConfigStatus = dict() + host_vars = self.vm.get_vars()['hostvars'][hostname] + k8_hostname = '' + if 'kubernetes.io/hostname' in host_vars.keys(): + k8_hostname = host_vars['kubernetes.io/hostname'] + else: + k8_hostname = hostname + if 'hostConfigStatus' in self.vm.get_vars()['hostvars']['localhost'].keys(): + hostConfigStatus = self.vm.get_vars()['hostvars']['localhost']['hostConfigStatus'] + if k8_hostname not in hostConfigStatus.keys(): + hostConfigStatus[k8_hostname] = dict() + if task_name in hostConfigStatus[k8_hostname].keys(): + status[task_name] = hostConfigStatus[k8_hostname][task_name] + status[task_name] = dict() + if 'stdout' in task_result.keys() and task_result['stdout'] != '': + status[task_name]['stdout'] = task_result['stdout'] + if 'stderr' in task_result.keys() and task_result['stderr'] != '': + status[task_name]['stderr'] = task_result['stderr'] + if 'msg' in task_result.keys() and task_result['msg'] != '': + status['msg'] = task_result['msg'].replace('\n', ' ') + if 'results' in task_result.keys() and len(task_result['results']) != 0: + status[task_name]['results'] = list() + for res in task_result['results']: + stat = dict() + if 'stdout' in res.keys() and res['stdout']: + stat['stdout'] = res['stdout'] + if 'stderr' in res.keys() and res['stderr']: + stat['stderr'] = res['stderr'] + if 'module_stdout' in res.keys() and res['module_stdout']: + stat['module_stdout'] = res['module_stdout'] + if 'module_stderr' in res.keys() and res['module_stderr']: + stat['module_stderr'] = res['module_stderr'] + if 'msg' in res.keys() and res['msg']: + stat['msg'] = res['msg'].replace('\n', ' ') + if 'item' in res.keys() and res['item']: + stat['item'] = res['item'] + if res['failed']: + stat['status'] = "Failed" + else: + stat['status'] = "Successful" + stat['stderr'] = "" + stat['module_stderr'] = "" + if "msg" not in stat.keys(): + stat['msg'] = "" + status[task_name]['results'].append(stat) + if failed: + status[task_name]['status'] = "Failed" + else: + status[task_name]['status'] = "Successful" + # As the k8s_status module is merging the current and previous status, if there are any previous failure messages overriding them https://github.com/fabianvf/ansible-k8s-status-module/blob/master/k8s_status.py#L322 + status[task_name]['stderr'] = "" + if "msg" not in status[task_name].keys(): + status[task_name]['msg'] = "" + hostConfigStatus[k8_hostname].update(status) + self.vm.set_host_variable('localhost', 'hostConfigStatus', hostConfigStatus) + self._display.display(str(status)) + return diff --git a/airship-host-config/plugins/filter/host_config_serial.py b/airship-host-config/plugins/filter/host_config_serial.py deleted file mode 100644 index 2f8d695..0000000 --- a/airship-host-config/plugins/filter/host_config_serial.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/python3 - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import json - -# Calculates the length of hosts in each groups -# Interested Groups are defined using the host_groups -# Returns a list of integers -def host_config_serial(host_groups, groups): - serial_list = list() - if type(host_groups) != list: - return '' - for i in host_groups: - if i in groups.keys(): - serial_list.append(str(len(groups[i]))) - return str(serial_list) - - -class FilterModule(object): - ''' HostConfig Serial plugin for ansible-operator ''' - - def filters(self): - return { - 'host_config_serial': host_config_serial - } diff --git a/airship-host-config/plugins/filter/host_config_serial_strategy.py b/airship-host-config/plugins/filter/host_config_serial_strategy.py deleted file mode 100644 index cdef40d..0000000 --- a/airship-host-config/plugins/filter/host_config_serial_strategy.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/python3 - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import json - -# Futher divides the host_config_serial variable into a new list -# so that for each iteration there will be not more than the -# strategy(int variable) number of hosts executing -def host_config_serial_strategy(strategy, host_groups, groups): - serial_list = list() - if type(strategy) != int and type(host_groups) != list: - return '' - for i in host_groups: - if i in groups.keys(): - length = len(groups[i]) - serial_list += int(length/strategy) * [strategy] - if length%strategy != 0: - serial_list.append(length%strategy) - return str(serial_list) - - -class FilterModule(object): - ''' HostConfig Serial Startegy plugin for ansible-operator to calucate the serial variable ''' - - def filters(self): - return { - 'host_config_serial_strategy': host_config_serial_strategy - } diff --git a/airship-host-config/plugins/filter/hostconfig_host_groups.py b/airship-host-config/plugins/filter/hostconfig_host_groups.py new file mode 100644 index 0000000..04ca5bc --- /dev/null +++ b/airship-host-config/plugins/filter/hostconfig_host_groups.py @@ -0,0 +1,88 @@ +#!/usr/bin/python3 + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import itertools +import os + +# This plugin calculates the list of list of hosts that need to be executed in +# sequence as given by the host_groups variable. The AND and OR conditions on the +# host_groups variable is calculated based on the match_host_groups variable +# This returns the list of list of hosts that the ansible should execute the playbook on +# Returns: [[192.168.1.12, 192.168.1.11], [192.168.1.14], [192.168.1.5]] + +def host_groups_get_keys(host_groups): + keys = list() + values = list() + for hg in host_groups: + keys.append(hg['name']) + values.append(hg['values']) + print(keys) + print(values) + return keys, values + +def host_groups_combinations(host_groups): + keys, values = host_groups_get_keys(host_groups) + for instance in itertools.product(*values): + yield dict(zip(keys, instance)) + +def removeSuccessHosts(hostGroups, hostConfigName): + filename = '/opt/ansible/data/hostconfig/'+hostConfigName+'/success_hosts' + print(filename) + if os.path.isfile(filename): + hosts = list() + with open(filename) as f: + hosts = [line.rstrip() for line in f] + print(hosts) + for host in hosts: + for hostGroup in hostGroups: + if host in hostGroup: + hostGroup.remove(host) + print(hostGroups) + return hostGroups + +def hostconfig_host_groups(host_groups, groups, hostConfigName, match_host_groups, reexecute): + host_groups_list = list() + host_group_list = list() + if type(host_groups) != list: + return '' + if match_host_groups: + hgs_list = list() + for host_group in host_groups_combinations(host_groups): + hg = list() + for k,v in host_group.items(): + hg.append(k+'_'+v) + hgs_list.append(hg) + for hgs in hgs_list: + host_group = groups[hgs[0]] + for i in range(1, len(hgs)): + host_group = list(set(host_group) & set(groups[hgs[i]])) + host_groups_list.append(host_group) + else: + for host_group in host_groups: + for value in host_group["values"]: + key = host_group["name"] + hg = list() + if key+'_'+value in groups.keys(): + if not host_group_list: + hg = groups[key+'_'+value] + host_group_list = hg.copy() + else: + hg = list((set(groups[key+'_'+value])) - (set(host_group_list) & set(groups[key+'_'+value]))) + host_group_list.extend(hg) + host_groups_list.append(hg) + else: + return "Invalid Host Groups "+key+" and "+value + if not reexecute: + return str(removeSuccessHosts(host_groups_list, hostConfigName)) + return str(host_groups_list) + + +class FilterModule(object): + ''' HostConfig Host Groups filter plugin for ansible-operator ''' + + def filters(self): + return { + 'hostconfig_host_groups': hostconfig_host_groups + } diff --git a/airship-host-config/plugins/filter/hostconfig_host_groups_to_list.py b/airship-host-config/plugins/filter/hostconfig_host_groups_to_list.py new file mode 100644 index 0000000..221647b --- /dev/null +++ b/airship-host-config/plugins/filter/hostconfig_host_groups_to_list.py @@ -0,0 +1,25 @@ +#!/usr/bin/python3 + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +# Converts the list of list of hosts to only a list of hosts +# that is accepted by the ansible playbook for execution +# Returns: [192.168.1.12, 192.168.1.11, 192.168.1.14, 192.168.1.5] + +def hostconfig_host_groups_to_list(hostconfig_host_groups): + host_groups_list = list() + if type(hostconfig_host_groups) != list: + return '' + for hg in hostconfig_host_groups: + host_groups_list.extend(hg) + return str(host_groups_list) + + +class FilterModule(object): + ''' Fake test plugin for ansible-operator ''' + + def filters(self): + return { + 'hostconfig_host_groups_to_list': hostconfig_host_groups_to_list + } diff --git a/airship-host-config/plugins/filter/hostconfig_hosts_parallel.py b/airship-host-config/plugins/filter/hostconfig_hosts_parallel.py new file mode 100644 index 0000000..161e22f --- /dev/null +++ b/airship-host-config/plugins/filter/hostconfig_hosts_parallel.py @@ -0,0 +1,42 @@ +#!/usr/bin/python3 + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +# Futher divides the host_config_serial variable into a new list +# so that for each iteration there will be not more than the +# max_hosts_parallel(int variable) number of hosts executing +# If we have 3 masters and 5 worker and labels sent are masters and workers +# and the max_hosts_parallel is 2 +# Returns: [2, 2, 2, 2] if sequential is false +# Returns: [2, 1, 2, 2, 1] if the sequential is true + +def hostconfig_max_hosts_parallel(max_hosts_parallel, hostconfig_host_groups, sequential=False): + parallel_list = list() + if type(max_hosts_parallel) != int and type(hostconfig_host_groups) != list and (sequential) != bool: + return '' + if sequential: + for hg in hostconfig_host_groups: + length = len(hg) + parallel_list += int(length/max_hosts_parallel) * [max_hosts_parallel] + if length%max_hosts_parallel != 0: + parallel_list.append(length%max_hosts_parallel) + else: + hgs = list() + for hg in hostconfig_host_groups: + hgs.extend(hg) + length = len(hgs) + parallel_list += int(length/max_hosts_parallel) * [max_hosts_parallel] + if length%max_hosts_parallel != 0: + parallel_list.append(length%max_hosts_parallel) + return str(parallel_list) + + +class FilterModule(object): + ''' HostConfig Max Hosts in Parallel plugin for ansible-operator to calucate the ansible serial variable ''' + + def filters(self): + return { + 'hostconfig_max_hosts_parallel': hostconfig_max_hosts_parallel + } + diff --git a/airship-host-config/plugins/filter/hostconfig_sequential.py b/airship-host-config/plugins/filter/hostconfig_sequential.py new file mode 100644 index 0000000..e9af788 --- /dev/null +++ b/airship-host-config/plugins/filter/hostconfig_sequential.py @@ -0,0 +1,26 @@ +#!/usr/bin/python3 + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +# Calculates the length of hosts in each groups +# Interested Groups are defined using the host_groups +# Returns a list of integers [2, 1, 3] based on the host_groups variables + +def hostconfig_sequential(hostconfig_host_groups, groups): + seq_list = list() + if type(hostconfig_host_groups) != list: + return '' + for host_group in hostconfig_host_groups: + if len(host_group) != 0: + seq_list.append(len(host_group)) + return str(seq_list) + + +class FilterModule(object): + ''' HostConfig Sequential plugin for ansible-operator ''' + + def filters(self): + return { + 'hostconfig_sequential': hostconfig_sequential + } diff --git a/airship-host-config/requirements.yml b/airship-host-config/requirements.yml index 8a661f8..d2af8e2 100644 --- a/airship-host-config/requirements.yml +++ b/airship-host-config/requirements.yml @@ -1,5 +1,3 @@ ---- collections: - - name: community.kubernetes - version: "<1.0.0" + - community.kubernetes - operator_sdk.util diff --git a/airship-host-config/roles/hostconfig/README.md b/airship-host-config/roles/hostconfig/README.md deleted file mode 100644 index c37ca91..0000000 --- a/airship-host-config/roles/hostconfig/README.md +++ /dev/null @@ -1,43 +0,0 @@ -Role Name -========= - -A brief description of the role goes here. - -Requirements ------------- - -Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, -if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. - -Role Variables --------------- - -A description of the settable variables for this role should go here, including any variables that are in -defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables -that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well - -Dependencies ------------- - -A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set -for other roles, or variables that are used from other roles. - -Example Playbook ----------------- - -Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for -users too: - - - hosts: servers - roles: - - { role: username.rolename, x: 42 } - -License -------- - -BSD - -Author Information ------------------- - -An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/airship-host-config/roles/hostconfig/defaults/main.yml b/airship-host-config/roles/hostconfig/defaults/main.yml deleted file mode 100644 index 2c3b2fb..0000000 --- a/airship-host-config/roles/hostconfig/defaults/main.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -# defaults file for hostconfig -message: "Hello" \ No newline at end of file diff --git a/airship-host-config/roles/hostconfig/handlers/main.yml b/airship-host-config/roles/hostconfig/handlers/main.yml deleted file mode 100644 index ab8cd2a..0000000 --- a/airship-host-config/roles/hostconfig/handlers/main.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -# handlers file for hostconfig diff --git a/airship-host-config/roles/hostconfig/meta/main.yml b/airship-host-config/roles/hostconfig/meta/main.yml deleted file mode 100644 index be34ca4..0000000 --- a/airship-host-config/roles/hostconfig/meta/main.yml +++ /dev/null @@ -1,64 +0,0 @@ ---- -galaxy_info: - author: your name - description: your description - company: your company (optional) - - # If the issue tracker for your role is not on github, uncomment the - # next line and provide a value - # issue_tracker_url: http://example.com/issue/tracker - - # Some suggested licenses: - # - BSD (default) - # - MIT - # - GPLv2 - # - GPLv3 - # - Apache - # - CC-BY - license: license (GPLv2, CC-BY, etc) - - min_ansible_version: 2.9 - - # If this a Container Enabled role, provide the minimum Ansible Container version. - # min_ansible_container_version: - - # Optionally specify the branch Galaxy will use when accessing the GitHub - # repo for this role. During role install, if no tags are available, - # Galaxy will use this branch. During import Galaxy will access files on - # this branch. If Travis integration is configured, only notifications for this - # branch will be accepted. Otherwise, in all cases, the repo's default branch - # (usually master) will be used. - #github_branch: - - # - # Provide a list of supported platforms, and for each platform a list of versions. - # If you don't wish to enumerate all versions for a particular platform, use 'all'. - # To view available platforms and versions (or releases), visit: - # https://galaxy.ansible.com/api/v1/platforms/ - # - # platforms: - # - name: Fedora - # versions: - # - all - # - 25 - # - name: SomePlatform - # versions: - # - all - # - 1.0 - # - 7 - # - 99.99 - - galaxy_tags: [] - # List tags for your role here, one per line. A tag is a keyword that describes - # and categorizes the role. Users find roles by searching for tags. Be sure to - # remove the '[]' above, if you add tags to this list. - # - # NOTE: A tag is limited to a single word comprised of alphanumeric characters. - # Maximum 20 tags per role. - -dependencies: [] - # List your role dependencies here, one per line. Be sure to remove the '[]' above, - # if you add dependencies to this list. -collections: -- operator_sdk.util -- community.kubernetes diff --git a/airship-host-config/roles/hostconfig/tasks/main.yml b/airship-host-config/roles/hostconfig/tasks/main.yml deleted file mode 100644 index 1f7cd30..0000000 --- a/airship-host-config/roles/hostconfig/tasks/main.yml +++ /dev/null @@ -1,25 +0,0 @@ ---- -# tasks file for hostconfig -- name: Hello World - debug: - msg: "Hello world from {{ meta.name }} in the {{ meta.namespace }} namespace." - -- name: Message - debug: - msg: "Message: {{ message }}" - -- name: DISPLAY HOST DETAILS - debug: - msg: "And the kubernetes node name is {{ kube_node_name }}, architecture is {{ architecture }} and kernel version is {{ kernel_version }}" - -- name: CREATING A FILE - shell: "hostname > ~/testing; date >> ~/testing;cat ~/testing;sleep 5" - register: output - -- debug: msg={{ output.stdout }} - -- name: ECHO HOSTNAME - shell: hostname - register: hostname - -- debug: msg={{ hostname.stdout }} diff --git a/airship-host-config/roles/hostconfig/vars/main.yml b/airship-host-config/roles/hostconfig/vars/main.yml deleted file mode 100644 index e848b0e..0000000 --- a/airship-host-config/roles/hostconfig/vars/main.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -# vars file for hostconfig diff --git a/airship-host-config/roles/setvariables/tasks/main.yml b/airship-host-config/roles/setvariables/tasks/main.yml new file mode 100644 index 0000000..beecf7c --- /dev/null +++ b/airship-host-config/roles/setvariables/tasks/main.yml @@ -0,0 +1,74 @@ +--- +# The below blocak of code helps in intializing the hosts, serial variables +# that would be used by the ansible playbook to control sequential or parallel execution +- name: Host Groups + block: + - set_fact: + reexecute: false + when: reexecute is undefined + - set_fact: + match_host_groups: false + when: match_host_groups is undefined + - set_fact: + sequential: false + when: sequential is undefined + # This hostconfig_host_groups custom filter plugin helps in computing the AND or OR + # operation on the host_groups labels passed through the CR object. + # The AND and OR operation is controlled using the match_host_groups variable. + # The function returns a list of list of hosts that need to be exexuted for + # every iteration. + # Returns: [[192.168.1.5, 192.168.1.3], [192.168.1.4]] + - set_fact: + hostconfig_host_groups: "{{ host_groups|hostconfig_host_groups(groups, meta.name, match_host_groups, reexecute) }}" + - debug: + msg: "Host Groups Variable {{ hostconfig_host_groups }}" + # The hostconfig_serial custom filter plugin helps in calculating the list of hosts + # that need to be executed for every iteration. The plugin uses the match_host_groups + # and sequential varaibale based on which the calculation is done. + # It returns a list of intergers having the number of hosts to be executed for each iteration + # Returns: [3, 4, 2] + - set_fact: + hostconfig_serial_variable: "{{ hostconfig_host_groups|hostconfig_sequential(groups) }}" + when: sequential is true + # Futher refining the hostconfig_serial_variable is done using this hostconfig_serial_strategy + # filter plugin in case the sequential flow is selected. + # Returns: [2, 1, 2, 2, 1] in case of sequential with 3 masters and 5 workers and max_hosts_parallel is 2 + - set_fact: + hostconfig_serial_variable: "{{ max_hosts_parallel|hostconfig_max_hosts_parallel(hostconfig_host_groups, sequential) }}" + when: max_hosts_parallel is defined + # The hostconfig_host_groups_to_list converts the hostconfig_serial_strategy + # to only a list of variables as accepted by the hosts field in ansible. + # This conversion is done here as the serial calculation would be easy if the + # variable is list of list earlier. + # Returns: [192.168.1.5, 192.168.1.3, 192.168.1.4] + - set_fact: + hostconfig_host_groups: "{{ hostconfig_host_groups|hostconfig_host_groups_to_list }}" + - debug: + msg: "Host Groups Variable {{ hostconfig_host_groups }}" + - debug: + msg: "Serial Variable {{ hostconfig_serial_variable }}" + when: hostconfig_serial_variable is defined + when: host_groups is defined +- name: Serial Strategy for parallel + block: + # This hostconfig_serial_strategy filter plugin helps to set the serial variable with + # fixed number of hosts for every execution in case of parallel execution. + # Returns: [2, 2, 2, 2] in case of parallel with 3 masters and 5 workers and max_hosts_parallel is 2 + - set_fact: + hostconfig_serial_variable: "{{ max_hosts_parallel|hostconfig_max_hosts_parallel([groups['all']]) }}" + - debug: + msg: "{{ hostconfig_serial_variable }}" + when: host_groups is undefined and max_hosts_parallel is defined +- name: Failure Testing + block: + # This block of max_failure_percentage helps in intaializing default value + # to the max_failure_percentage variable so that the below play would be selected + # appropriately + - set_fact: + max_failure_percentage: "{{ max_failure_percentage }}" + when: max_failure_percentage is defined + # Please note we are just setting some default value to the max_failure_percentage + # so that we can check the conditions below + - set_fact: + max_failure_percentage: 100 + when: max_failure_percentage is undefined diff --git a/airship-host-config/roles/sysctl/tasks/main.yml b/airship-host-config/roles/sysctl/tasks/main.yml new file mode 100644 index 0000000..918efc6 --- /dev/null +++ b/airship-host-config/roles/sysctl/tasks/main.yml @@ -0,0 +1,15 @@ +--- +- name: sysctl configuration + sysctl: + name: "{{ item.name }}" + value: "{{ item.value }}" + sysctl_set: yes + state: present + reload: yes + with_items: "{{ config.sysctl }}" + become: yes + register: sysctl_output + +- name: sysctl output + debug: msg={{ sysctl_output }} + when: sysctl_output is defined diff --git a/airship-host-config/roles/ulimit/tasks/main.yml b/airship-host-config/roles/ulimit/tasks/main.yml new file mode 100644 index 0000000..e4aed88 --- /dev/null +++ b/airship-host-config/roles/ulimit/tasks/main.yml @@ -0,0 +1,14 @@ +--- +- name: ulimit configuration + pam_limits: + domain: "{{ item.user }}" + limit_type: "{{ item.type }}" + limit_item: "{{ item.item }}" + value: "{{ item.value }}" + with_items: "{{ config.ulimit }}" + become: yes + register: ulimit_output + +- name: ulimit output + debug: msg={{ ulimit_output }} + when: ulimit_output is defined diff --git a/airship-host-config/setup.sh b/airship-host-config/setup.sh index ae8b5bd..f87766a 100755 --- a/airship-host-config/setup.sh +++ b/airship-host-config/setup.sh @@ -31,10 +31,12 @@ save_and_load_docker_image(){ worker_node_ips=$(get_worker_ips) echo "Copying Image to following worker Nodes" echo $worker_node_ips + touch $HOME/hello for i in $worker_node_ips do sshpass -p "vagrant" scp -o StrictHostKeyChecking=no $IMAGE_NAME vagrant@$i:~/. sshpass -p "vagrant" ssh vagrant@$i docker load -i $IMAGE_NAME + sshpass -p "vagrant" ssh vagrant@$i touch hello done } diff --git a/airship-host-config/watches.yaml b/airship-host-config/watches.yaml index 1e594e2..c0ff4ea 100644 --- a/airship-host-config/watches.yaml +++ b/airship-host-config/watches.yaml @@ -2,5 +2,7 @@ - version: v1alpha1 group: hostconfig.airshipit.org kind: HostConfig - # role: hostconfig - playbook: /opt/ansible/playbook.yaml + playbook: playbooks/create_playbook.yaml + finalizer: + name: finalizer.hostconfig.airshipit.org + playbook: playbooks/delete_playbook.yaml diff --git a/docs/CR_creation_flow.png b/docs/CR_creation_flow.png new file mode 100644 index 0000000000000000000000000000000000000000..5f709187103f0c126ddde0adc036733783623120 GIT binary patch literal 32955 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 literal 0 HcmV?d00001 diff --git a/docs/Deployment_Architecture.png b/docs/Deployment_Architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..b3687f627c15cf2603e559e44c69accf17cf314b GIT binary patch literal 41430 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μ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 + +## Architecture + +![](Deployment_Architecture.png) + + +Hostconfig operator will be running as a kubernetes deployment on the target kubernetes cluster. + +**Hostconfig Operator Code** + +The code base for the ansible operator is available at: https://github.com/SirishaGopigiri/airship-host-config/tree/integration + +The repository also have vagrants scripts to build kubernetes cluster on the Vagrant VMs and then test the ansible-operator pod. + +## Deployment and Host Configuration Flow + +The hostconfig operator deployment sequence + +![](deployment_flow.png) + +Using operator pod to perform host configuration on kubernetes nodes + +![](CR_creation_flow.png) + + +## How to Deploy(On existing kubernetes cluster) + +**Pre-requisite:** + +1. The Kubernetes nodes should be labelled with any one of the below label to execute based on host-groups, if not labelled by default executes on all the nodes as no selection happens. + Valid labels: + * [`topology.kubernetes.io/region`](https://kubernetes.io/docs/reference/kubernetes-api/labels-annotations-taints/#topologykubernetesiozone) + * [`topology.kubernetes.io/zone`](https://kubernetes.io/docs/reference/kubernetes-api/labels-annotations-taints/#topologykubernetesioregion) + * `kubernetes.io/role` + * [`kubernetes.io/hostname`](https://kubernetes.io/docs/reference/kubernetes-api/labels-annotations-taints/#kubernetes-io-hostname) + * [`kubernetes.io/arch`](https://kubernetes.io/docs/reference/kubernetes-api/labels-annotations-taints/#kubernetes-io-arch) + * [`kubernetes.io/os`](https://kubernetes.io/docs/reference/kubernetes-api/labels-annotations-taints/#kubernetes-io-os) + +2. **Operator pod connecting to Kubernetes Nodes:** + + The kubernetes nodes should be annotated with secret name having the username and private key as part of the contents. +2. **Operator pod connecting to Kubernetes Nodes:** + + The kubernetes nodes should be annotated with secret name having the username and private key as part of the contents. + +git clone the hostconfig repository + +`git clone -b integration https://github.com/SirishaGopigiri/airship-host-config.git` + +Move to airship-host-config directory + +`cd airship-host-config/airship-host-config` + +Create a HostConfig CRD + +`kubectl create -f deploy/crds/hostconfig.airshipit.org_hostconfigs_crd.yaml` + +Create hostconfig role, service account, role-binding and cluster-role-binding which is used to deploy and manage the operations done using the hostconfig operator pod + +`kubectl create -f deploy/role.yaml` +`kubectl create -f deploy/service_account.yaml` +`kubectl create -f deploy/role_binding.yaml` +`kubectl create -f deploy/cluster_role_binding.yaml` + +Now deploy the hostconfig operator pod + +`kubectl create -f deploy/operator.yaml` + +Once the hostconfig operator pod is deployed, we can create the desired HostConfig CR with the required configuration. And this CR can be passed to the operator pod which performs the required operation. + +Some example CRs are available in the demo_examples directory. + +## Airshipctl integration + +The hostconfig operator can be integrated with airshipctl code by changing the manifests folder, which are used to build the target workload cluster. + +For the proposed changes, please refer to below Patch set: + +https://review.opendev.org/#/c/744098 + +To deploy the operator on the cluster using `airshipctl phase apply`, the function needs to be invoked from the corresponding kuztomization.yaml file. This is WIP. + +## References + +1. https://docs.openshift.com/container-platform/4.1/applications/operator_sdk/osdk-ansible.html +2. https://github.com/operator-framework/operator-sdk +3. https://docs.ansible.com/ansible/latest/modules/sysctl_module.html +4. https://docs.ansible.com/ansible/latest/modules/pam_limits_module.html diff --git a/docs/deployment_flow.png b/docs/deployment_flow.png new file mode 100644 index 0000000000000000000000000000000000000000..8c0c37296fb95b339ec4616139f9579ff3790c94 GIT binary patch literal 20487 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 literal 0 HcmV?d00001 diff --git a/kubernetes/README.md b/kubernetes/README.md index 1cb069d..1ab97da 100644 --- a/kubernetes/README.md +++ b/kubernetes/README.md @@ -11,7 +11,7 @@ A vagrant script for setting up a Kubernetes cluster using Kubeadm Git clone the repo on the host machine which has vagrant and virtual box installed ``` -git clone https://github.com/SirishaGopigiri/airship-host-config.git +git clone https://github.com/SirishaGopigiri/airship-host-config.git -b june_29 ``` Navigate to the kubernetes folder @@ -20,7 +20,7 @@ Navigate to the kubernetes folder cd airship-host-config/kubernetes/ ``` -Execute the following vagrant command to start a new Kubernetes cluster, this will start one master and two nodes: +Execute the following vagrant command to start a new Kubernetes cluster, this will start three master and five nodes: ``` vagrant up @@ -28,7 +28,33 @@ vagrant up You can also start invidual machines by vagrant up k8s-head, vagrant up k8s-node-1 and vagrant up k8s-node-2 -If more than two nodes are required, you can edit the servers array in the Vagrantfile +If you would need more master nodes, you can edit the servers array in the Vagrantfile. Please change the name, and IP address for eth1. +``` +servers = [ + { + :name => "k8s-master-1", + :type => "master", + :box => "ubuntu/xenial64", + :box_version => "20180831.0.0", + :eth1 => "192.168.205.10", + :mem => "2048", + :cpu => "2" + } +] +``` +Also update the haproxy.cfg file to add more master servers. + +``` + balance roundrobin + server k8s-api-1 192.168.205.10:6443 check + server k8s-api-2 192.168.205.11:6443 check + server k8s-api-3 192.168.205.12:6443 check + server k8s-api-4 check +``` + + +If more than five nodes are required, you can edit the servers array in the Vagrantfile. Please chang ethe name, an +d IP address for eth1. ``` servers = [ @@ -37,7 +63,7 @@ servers = [ :type => "node", :box => "ubuntu/xenial64", :box_version => "20180831.0.0", - :eth1 => "192.168.205.13", + :eth1 => "192.168.205.14", :mem => "2048", :cpu => "2" } diff --git a/kubernetes/Vagrantfile b/kubernetes/Vagrantfile index 259b176..7129eb4 100644 --- a/kubernetes/Vagrantfile +++ b/kubernetes/Vagrantfile @@ -3,7 +3,16 @@ servers = [ { - :name => "k8s-master", + :name => "k8s-lbhaproxy", + :type => "lbhaproxy", + :box => "ubuntu/xenial64", + :box_version => "20180831.0.0", + :eth1 => "192.168.205.13", + :mem => "2048", + :cpu => "2" + }, + { + :name => "k8s-master-1", :type => "master", :box => "ubuntu/xenial64", :box_version => "20180831.0.0", @@ -11,12 +20,30 @@ servers = [ :mem => "2048", :cpu => "2" }, + { + :name => "k8s-master-2", + :type => "master-join", + :box => "ubuntu/xenial64", + :box_version => "20180831.0.0", + :eth1 => "192.168.205.11", + :mem => "2048", + :cpu => "2" + }, + { + :name => "k8s-master-3", + :type => "master-join", + :box => "ubuntu/xenial64", + :box_version => "20180831.0.0", + :eth1 => "192.168.205.12", + :mem => "2048", + :cpu => "2" + }, { :name => "k8s-node-1", :type => "node", :box => "ubuntu/xenial64", :box_version => "20180831.0.0", - :eth1 => "192.168.205.11", + :eth1 => "192.168.205.14", :mem => "2048", :cpu => "2" }, @@ -25,7 +52,34 @@ servers = [ :type => "node", :box => "ubuntu/xenial64", :box_version => "20180831.0.0", - :eth1 => "192.168.205.12", + :eth1 => "192.168.205.15", + :mem => "2048", + :cpu => "2" + }, + { + :name => "k8s-node-3", + :type => "node", + :box => "ubuntu/xenial64", + :box_version => "20180831.0.0", + :eth1 => "192.168.205.16", + :mem => "2048", + :cpu => "2" + }, + { + :name => "k8s-node-4", + :type => "node", + :box => "ubuntu/xenial64", + :box_version => "20180831.0.0", + :eth1 => "192.168.205.17", + :mem => "2048", + :cpu => "2" + }, + { + :name => "k8s-node-5", + :type => "node", + :box => "ubuntu/xenial64", + :box_version => "20180831.0.0", + :eth1 => "192.168.205.18", :mem => "2048", :cpu => "2" } @@ -83,7 +137,7 @@ $configureMaster = <<-SCRIPT # install k8s master HOST_NAME=$(hostname -s) - kubeadm init --apiserver-advertise-address=$IP_ADDR --apiserver-cert-extra-sans=$IP_ADDR --node-name $HOST_NAME --pod-network-cidr=172.16.0.0/16 + kubeadm init --apiserver-advertise-address=$IP_ADDR --apiserver-cert-extra-sans=$IP_ADDR --node-name $HOST_NAME --pod-network-cidr=172.16.0.0/16 --control-plane-endpoint "192.168.205.13:443" --upload-certs #copying credentials to regular user - vagrant sudo --user=vagrant mkdir -p /home/vagrant/.kube @@ -94,14 +148,29 @@ $configureMaster = <<-SCRIPT export KUBECONFIG=/etc/kubernetes/admin.conf kubectl apply -f https://raw.githubusercontent.com/SirishaGopigiri/airship-host-config/master/kubernetes/calico/calico.yaml + kubeadm init phase upload-certs --upload-certs > /etc/upload_cert kubeadm token create --print-join-command >> /etc/kubeadm_join_cmd.sh chmod +x /etc/kubeadm_join_cmd.sh + cat /etc/kubeadm_join_cmd.sh > /etc/kubeadm_join_master.sh + CERT=`tail -1 /etc/upload_cert` + sed -i '$ s/$/ --control-plane --certificate-key '"$CERT"'/' /etc/kubeadm_join_master.sh + #Install sshpass for futher docker image copy apt-get install -y sshpass SCRIPT +$configureMasterJoin = <<-SCRIPT + echo -e "\nThis is Master with Join Commadn:\n" + apt-get install -y sshpass + sshpass -p "vagrant" scp -o StrictHostKeyChecking=no vagrant@192.168.205.10:/etc/kubeadm_join_master.sh . + IP_ADDR=`ifconfig enp0s8 | grep Mask | awk '{print $2}'| cut -f2 -d:` + + sed -i '$ s/$/ --apiserver-advertise-address '"$IP_ADDR"'/' kubeadm_join_master.sh + sh ./kubeadm_join_master.sh +SCRIPT + $configureNode = <<-SCRIPT echo -e "\nThis is worker:\n" apt-get install -y sshpass @@ -123,21 +192,24 @@ Vagrant.configure("2") do |config| config.vm.provider "virtualbox" do |v| v.name = opts[:name] - v.customize ["modifyvm", :id, "--groups", "/Ballerina Development"] + v.customize ["modifyvm", :id, "--groups", "/Ballerina Development"] v.customize ["modifyvm", :id, "--memory", opts[:mem]] v.customize ["modifyvm", :id, "--cpus", opts[:cpu]] end - # we cannot use this because we can't install the docker version we want - https://github.com/hashicorp/vagrant/issues/4871 - # config.vm.provision "docker" - - config.vm.provision "shell", inline: $configureBox if opts[:type] == "master" + config.vm.provision "shell", inline: $configureBox config.vm.provision "shell", inline: $configureMaster config.vm.provision "file", source: "../airship-host-config", destination: "/home/vagrant/airship-host-config/airship-host-config" + elsif opts[:type] == "lbhaproxy" + config.vm.provision "shell", :path => "haproxy.sh" + elsif opts[:type] == "master-join" + config.vm.provision "shell", inline: $configureBox + config.vm.provision "shell", inline: $configureMasterJoin else + config.vm.provision "shell", inline: $configureBox config.vm.provision "shell", inline: $configureNode end diff --git a/kubernetes/haproxy.sh b/kubernetes/haproxy.sh new file mode 100644 index 0000000..71587ce --- /dev/null +++ b/kubernetes/haproxy.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +if [ ! -f /etc/haproxy/haproxy.cfg ]; then + + # Install haproxy + sudo sed -i "/^[^#]*PasswordAuthentication[[:space:]]no/c\PasswordAuthentication yes" /etc/ssh/sshd_config + sudo service sshd restart + /usr/bin/apt-get -y install haproxy + cp /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.orig + + # Configure haproxy + cat > /etc/default/haproxy < /etc/haproxy/haproxy.cfg <