diff --git a/roles/cnv_instances/defaults/main.yml b/roles/cnv_instances/defaults/main.yml new file mode 100644 index 0000000..e295051 --- /dev/null +++ b/roles/cnv_instances/defaults/main.yml @@ -0,0 +1,33 @@ +--- +# If your workload needs customization options provide variables to be used. +# +# Variable names must be prefixed by the role name, e.g. "_". +# +# Therefore because this example workload is called "ns_example" the variables +# must be named "ns_example_variable". +# +# You can override the defaults as parameters to the Ansible run that deploys +# your workload via the AgnosticV configuration. + +cnv_instances: [] + +instances: "{{ cnv_instances }}" + +openshift_cnv_namespace: "{{ sandbox_openshift_namespace }}" + +# Naming of instances +openshift_cnv_instance_numeration: '%d' + +# Delete device retry +openshift_cnv_delete_retries: 20 +openshift_cnv_delete_delay: 60 + +# Retries and delay variable +openshift_cnv_retries: 6 +openshift_cnv_delay: 10 + +# Remove X-Frame-Options header (to iframe apps, for example in showroom) +openshift_cnv_route_remove_x_frame_options_header: false + +# Use host.guid.domain +openshift_cnv_legacy_routes: false diff --git a/roles/cnv_instances/meta/main.yml b/roles/cnv_instances/meta/main.yml new file mode 100644 index 0000000..8082e18 --- /dev/null +++ b/roles/cnv_instances/meta/main.yml @@ -0,0 +1,13 @@ +--- +galaxy_info: + role_name: cnv_instances + author: Red Hat, Alberto Gonzalez (josegonz@redhat.com) + description: | + Example workload to be deployed onto OpenShift 4 + license: MIT + min_ansible_version: "2.9" + platforms: [] + galaxy_tags: + - ocp + - openshift +dependencies: [] diff --git a/roles/cnv_instances/readme.adoc b/roles/cnv_instances/readme.adoc new file mode 100644 index 0000000..f1613d2 --- /dev/null +++ b/roles/cnv_instances/readme.adoc @@ -0,0 +1,37 @@ += cnv_instances - Create instances on CNV cluster + +== Role overview + +* This is a working no-op role that can be used as a template to develop new workload roles to deploy to a cnv instances config. It consists of the following tasks files: + +** Tasks: link:./tasks/workload.yml[workload.yml] - Used to deploy the actual workload, i.e, 3scale, some Demo or OpenShift customization +*** This role only prints the current username for which this role is provisioning. + +** Tasks: link:./tasks/remove_workload.yml[remove_workload.yml] - Used to delete the workload +*** This role doesn't do anything here + +The provisioning infrastructure will set a variable `ACTION` to be either `provision` or `destroy` depending on the operation. + +== The defaults variable file + +=== Variable Naming + +Since Ansible lacks robust variable scoping you *must* use long-name scope parameters for your workload to avoid variable clashing. + +For example, parameters named `ns_example_*` would be recognized as unique to this workload. + +* This file link:./defaults/main.yml[./defaults/main.yml] contains all the variables you need to define to control the deployment of your workload. +* The variable *ocp_username* is mandatory to assign the workload to the correct OpenShift user when deploying to a shared cluster. For most workloads the default of `system:admin` is the correct value unless this is a workload to be applied to a shared cluster. +* You can modify any of these default values by adding `-e "variable_name=variable_value"` to the command line +* Your deployer will override any of these variables based on configuration in AgnosticV +* Add long-name scoped workload parameters. Example: `ns_example_machineconfigpool_name: worker` + +== The internal variable file + +If the workload needs to set some internal variables define them in the file link:./vars/main.yml[./vars/main.yml]. + +Variables should be named just like external variables with the exception that they should start with an underscore (`_`). + +Examples: + +* `cnv_instances` diff --git a/roles/cnv_instances/tasks/create_instance.yaml b/roles/cnv_instances/tasks/create_instance.yaml new file mode 100644 index 0000000..fc69637 --- /dev/null +++ b/roles/cnv_instances/tasks/create_instance.yaml @@ -0,0 +1,229 @@ +--- +- name: Empty variable _instance_ + ansible.builtin.set_fact: + _instance_interfaces: [] + _instance_networks: [] + _instance_volumes: + - dataVolume: + name: "INSTANCENAME-{{guid}}" + name: "INSTANCENAME-{{guid}}" + _instance_disks: + - disk: + bus: "{{ _instance.disk_type | default('virtio') }}" + name: "INSTANCENAME-{{guid}}" + +- name: Set the instances interfaces + ansible.builtin.set_fact: + _instance_interfaces: >- + {{ + _instance_interfaces + [{ + 'name': _network.split('/')[1] if '/' in _network else _network + guid, + 'macAddress': _instance.fixed_macs[_network] | default('2c:c2:60' | random_mac), + 'bridge': {}, + 'model': _instance.interface_model | default('virtio'), + } + if _network != 'default' + else { + 'name': 'default', + 'model': _instance.interface_model | default('virtio'), + 'masquerade': {} + }] + }} + _instance_networks: >- + {{ _instance_networks + [ + { + 'name': _network.split('/')[1] if '/' in _network else _network + guid, + 'multus': {'networkName': _network if '/' in _network else _network + guid} + } + if _network != 'default' + else { + 'name': 'default', + 'pod': {} + } + ] }} + loop: "{{ _instance.networks | default(['default']) | list }}" + loop_control: + loop_var: _network + index_var: _network_idx + +- name: Set the instances disks + ansible.builtin.set_fact: + _instance_disks: "{{ _instance_disks | from_yaml + [ + { + 'name': _disk.metadata.name, + 'disk': {'bus': _instance.disk_type | default('virtio')} + } + ] }}" + _instance_volumes: "{{ _instance_volumes | from_yaml + [ + { + 'name': _disk.metadata.name, + 'dataVolume': {'name': _disk.metadata.name} + } + ] }}" + loop: "{{ _instance.disks | default([]) | list }}" + loop_control: + loop_var: _disk + +- name: Set the instances cdroms + ansible.builtin.set_fact: + _instance_disks: "{{ _instance_disks | from_yaml + [ + { + 'name': _disk.metadata.name, + 'cdrom': {'bus': 'sata'} + } + ] }}" + _instance_volumes: "{{ _instance_volumes | from_yaml + [ + { + 'name': _disk.metadata.name, + 'dataVolume': {'name': _disk.metadata.name} + } + ] }}" + loop: "{{ _instance.cdroms | default([]) | list }}" + loop_control: + loop_var: _disk + + +- name: Set cloud_config + ansible.builtin.set_fact: + _cloud_config: |- + #cloud-config + ssh_authorized_keys: + {{ _instance.userdata | default('') | replace('#cloud-config','') | replace('INSTANCEGUID', guid) | default('') }} + +- name: Set cloud init disk if needed + when: _instance.disable_cloud_init | default(false) == false + set_fact: + _instance_volumes: "{{ _instance_volumes | from_yaml + [ + { + 'name': 'cloudinitdisk', + 'cloudInitNoCloud': { + 'userDataBase64': _cloud_config | b64encode, + 'networkDataBase64': _instance.networkdata | default('network: 2') | b64encode + } + } + ] }}" + _instance_disks: "{{ _instance_disks | from_yaml + [ + { + 'disk': {'bus': 'virtio'}, + 'name': 'cloudinitdisk' + } + ] }}" + +- name: Fail if image (pvc) doesn't exist on namespace cnv-images + when: _instance.image_type | default('pvc') == 'pvc' + kubernetes.core.k8s_info: + api_version: v1 + kind: PersistentVolumeClaim + name: "{{ _instance.image }}" + namespace: cnv-images + register: r_pvc_info + +- name: Fail if PVC does not exist + ansible.builtin.fail: + msg: "PersistentVolumeClaim {{ _instance.image }} does not exist in namespace cnv-images" + when: + - _instance.image_type | default('pvc') == 'pvc' + - r_pvc_info.resources | default([]) | length == 0 + + +- name: Create instance(s) "{{ _instance.name }}" + vars: + _instance_name: "{{ _instance.name }}{{ _index+1 if _instance.count|d(1)|int > 1 }}" + _datavolume: | + - metadata: + name: "{{ _instance_name }}-{{ guid }}" + spec: + source: + {% if _instance.image_type | default('pvc') == 'pvc' %} + pvc: + namespace: "cnv-images" + name: "{{ _instance.image }}" + {% elif _instance.image_type | default('pvc') == 'registry' %} + registry: + url: "{{ _instance.image }}" + {% elif _instance.image_type | default('pvc') in ['url','http'] %} + http: + url: "{{ _instance.image }}" + {% elif _instance.image_type | default('pvc') == 'blank' %} + blank: {} + {% endif %} + pvc: + accessModes: + - ReadWriteMany + volumeMode: Block + resources: + requests: + storage: "{{ _instance.image_size }}" + _spec: | + hostname: "{{ _instance_name }}" + subdomain: "{{ 'lab' if openshift_cnv_add_lab_subdomain | default(True) else '' }}" + domain: + firmware: + uuid: "{{ 99999999 | random | to_uuid }}" + bootloader: + {% if _instance.bootloader | default('bios') == 'bios' %} + bios: {} + {% elif _instance.bootloader | default('bios') in ['efi', 'uefi'] %} + efi: + secureBoot: false + {% endif %} + cpu: + cores: {{ _instance.cores }} + model: host-passthrough + machine: + type: "{{ _instance.machine_type | default('pc-q35-rhel9.2.0') }}" + memory: + guest: "{{ _instance.memory }}" + memory: + hugepages: + pageSize: "1Gi" + devices: + disks: {{ _instance_disks | replace('INSTANCENAME', _instance_name) }} + interfaces: {{ _instance_interfaces }} + networks: {{ _instance_networks }} + volumes: {{ _instance_volumes | replace('INSTANCENAME', _instance_name) }} + kubernetes.core.k8s: + definition: + apiVersion: kubevirt.io/v1 + kind: VirtualMachine + metadata: + name: "{{ _instance_name }}" + namespace: "{{ openshift_cnv_namespace }}" + annotations: >- + {{ _instance.metadata|default({}) | combine(_instance.tags|default({})) | default({}) }} + spec: + dataVolumeTemplates: >- + {{ _datavolume | from_yaml + (_instance.disks | default([]) | to_json | replace('INSTANCENAME',_instance_name) | from_json) + (_instance.cdroms | default([]) | to_json | replace('INSTANCENAME',_instance_name) | from_json) }} + running: true + template: + metadata: + labels: + vm.cnv.io/name: "{{ _instance_name }}" + spec: "{{ _spec | from_yaml }}" + loop: "{{ range(1, _instance.count|default(1)|int+1) | list }}" + loop_control: + index_var: _index + register: r_openshift_cnv_instance + until: r_openshift_cnv_instance is success + retries: "{{ openshift_cnv_retries }}" + delay: "{{ openshift_cnv_delay }}" + +- name: Wait till VM is running + vars: + _instance_name: "{{ _instance.name }}{{ _index+1 if _instance.count | default(1) | int > 1 }}" + kubernetes.core.k8s_info: + api_version: kubevirt.io/v1 + kind: VirtualMachine + name: "{{ _instance_name }}" + namespace: "{{ openshift_cnv_namespace }}" + register: r_vm_status + until: r_vm_status.resources[0].status.printableStatus | default('') == "Running" + retries: 30 + delay: 10 + loop: "{{ range(1, _instance.count | default(1) | int+1) | list }}" + loop_control: + index_var: _index + +- ansible.builtin.set_fact: + r_openshift_cnv_instances: "{{ r_openshift_cnv_instances + [item] }}" + loop: "{{ r_openshift_cnv_instance.results | list }}" diff --git a/roles/cnv_instances/tasks/create_routes.yaml b/roles/cnv_instances/tasks/create_routes.yaml new file mode 100644 index 0000000..d5ec446 --- /dev/null +++ b/roles/cnv_instances/tasks/create_routes.yaml @@ -0,0 +1,85 @@ +--- +- name: Create routes with tls + kubernetes.core.k8s: + definition: + apiVersion: route.openshift.io/v1 + kind: Route + metadata: + name: "{{ route.name }}" + namespace: "{{ openshift_cnv_namespace }}" + spec: + host: >- + {{ + (route.host | default(route.name) ~ '.' ~ guid ~ '.' ~ sandbox_openshift_apps_domain) + if (openshift_cnv_legacy_routes | default(false) and route.legacy | default(true)) or route.legacy | default(false) + else + (route.host | default(route.name) ~ '-' ~ guid ~ '.' ~ sandbox_openshift_apps_domain) + }} + port: + targetPort: "{{ route.targetPort }}" + tls: + termination: "{{ route.tls_termination | default('passthrough') }}" + insecureEdgeTerminationPolicy: None + destinationCACertificate: "{{ route.tls_destinationCACertificate | default('') }}" + to: + kind: Service + name: "{{ route.service }}" + httpHeaders: "{{ + ( + openshift_cnv_route_remove_x_frame_options_header + and (route.tls_termination | default('passthrough') != 'passthrough') + ) + | ternary( + { + 'actions': { + 'response': [ + { 'name': 'X-Frame-Options', 'action': { 'type': 'Delete' } }, + { 'name': 'Content-Security-Policy', 'action': { 'type': 'Delete' } }, + { 'name': 'referrer-policy', 'action': { 'type': 'Delete' } } + ] + } + }, + omit + ) + }}" + when: + - route.tls|default(False) == True + loop: "{{ _instance.routes|default([]) }}" + loop_control: + loop_var: route + register: r_createroute + until: r_createroute is success + retries: "{{ openshift_cnv_retries }}" + delay: "{{ openshift_cnv_delay }}" + +- name: Create routes without tls + kubernetes.core.k8s: + definition: + apiVersion: route.openshift.io/v1 + kind: Route + metadata: + name: "{{ route.name }}" + namespace: "{{ openshift_cnv_namespace }}" + spec: + host: >- + {{ + (route.host | default(route.name) ~ '.' ~ guid ~ '.' ~ sandbox_openshift_apps_domain) + if openshift_cnv_legacy_routes | default(false) + else + (route.host | default(route.name) ~ '-' ~ guid ~ '.' ~ sandbox_openshift_apps_domain) + }} + path: "{{ route.path|default('/') }}" + port: + targetPort: "{{ route.targetPort }}" + to: + kind: Service + name: "{{ route.service }}" + httpHeaders: "{{ (openshift_cnv_route_remove_x_frame_options_header) | ternary({'actions': {'response': [{'name': 'X-Frame-Options', 'action': {'type': 'Delete'}}]}}, omit) }}" + when: "{{ route.tls|default(false) == false }}" + loop: "{{ _instance.routes|default([]) }}" + loop_control: + loop_var: route + register: r_createroute + until: r_createroute is success + retries: "{{ openshift_cnv_retries }}" + delay: "{{ openshift_cnv_delay }}" diff --git a/roles/cnv_instances/tasks/create_service.yaml b/roles/cnv_instances/tasks/create_service.yaml new file mode 100644 index 0000000..e3eca98 --- /dev/null +++ b/roles/cnv_instances/tasks/create_service.yaml @@ -0,0 +1,103 @@ +--- +- name: Create services for instance "{{ _instance.name }}" + kubernetes.core.k8s: + definition: + apiVersion: v1 + kind: Service + metadata: + name: "{{ service.name }}" + namespace: "{{ openshift_cnv_namespace }}" + spec: "{{ spec | from_yaml }}" + vars: + spec: | + ports: + {{ service.ports }} + selector: + vm.cnv.io/name: "{{ _instance.name }}" + type: {{ service.type | default('ClusterIP') }} + register: r_service + until: r_service is success + retries: "{{ openshift_cnv_retries }}" + delay: "{{ openshift_cnv_delay }}" + +- name: Add passthrough service to the nested OCP cluster + when: cnv_instances_passthrough_namespace | default("") != "" + module_defaults: + group/k8s: + host: "{{ openshift_api_url }}" + api_key: "{{ openshift_cluster_admin_token }}" + validate_certs: false + block: + - name: Add Service + kubernetes.core.k8s: + definition: + apiVersion: v1 + kind: Service + metadata: + name: "{{ service.name }}" + namespace: "{{ cnv_instances_passthrough_namespace }}" + spec: "{{ spec | from_yaml }}" + vars: + spec: | + ports: + {{ service.ports }} + type: {{ service.type | default('ClusterIP') }} + - name: Add EndpointSlice + kubernetes.core.k8s: + definition: + apiVersion: discovery.k8s.io/v1 + kind: EndpointSlice + metadata: + name: "{{ service.name }}" + namespace: "{{ cnv_instances_passthrough_namespace }}" + labels: + kubernetes.io/service-name: "{{ service.name }}" + addressType: IPv4 + endpoints: + - addresses: + - "{{ r_service.result.spec.clusterIP }}" + conditions: + ready: true + ports: "{{ service.ports }}" + +- name: Wait for the LoadBalancer value + register: svc_fip + kubernetes.core.k8s_info: + api_version: v1 + kind: Service + name: "{{ service.name }}" + namespace: "{{ openshift_cnv_namespace }}" + until: svc_fip.resources[0].status.loadBalancer.ingress[0].ip | default('') != '' + retries: 10 + delay: 2 + when: + - service.type | default('ClusterIP') == "LoadBalancer" + +- name: If exist set the external IP for the instance and the ports + set_fact: + _instance_external_ip: "{{ svc_fip.resources[0].status.loadBalancer.ingress[0].ip }}" + _instance_external_ports: "{{ service.ports | map(attribute='port') | join(',') }}" + when: + - service.type | default('ClusterIP') == "LoadBalancer" + +- name: Add external_ip and external_ports to the instance + when: + - service.type | default('ClusterIP') == "LoadBalancer" + kubernetes.core.k8s: + state: patched + api_version: v1 + kind: VirtualMachine + name: "{{ _instance.name }}" + definition: + apiVersion: v1 + kind: VirtualMachine + metadata: + name: "{{ _instance.name }}" + namespace: "{{ openshift_cnv_namespace }}" + annotations: + external_ip: "{{ _instance_external_ip }}" + external_ports: "{{ _instance_external_ports }}" + register: r_service + until: r_service is success + retries: "{{ openshift_cnv_retries }}" + delay: "{{ openshift_cnv_delay }}" diff --git a/roles/cnv_instances/tasks/create_services.yaml b/roles/cnv_instances/tasks/create_services.yaml new file mode 100644 index 0000000..d1558e3 --- /dev/null +++ b/roles/cnv_instances/tasks/create_services.yaml @@ -0,0 +1,60 @@ +- name: Create a SSH (or defined one) service for internal connections for node {{ _instance.name }} + vars: + _instance_name: "{{ _instance.name }}{{ _index+1 if _instance.count|d(1)|int > 1 }}" + _definition: | + apiVersion: v1 + kind: Service + metadata: + name: "{{ _instance_name }}" + namespace: "{{ openshift_cnv_namespace }}" + spec: + ports: + - port: {{ _instance.servicePort | default(22) |int}} + protocol: TCP + targetPort: {{ _instance.servicePort | default(22)|int }} + selector: + vm.cnv.io/name: "{{ _instance_name }}" + + kubernetes.core.k8s: + definition: "{{ _definition }}" + loop: "{{ range(1, _instance.count|default(1)|int+1) | list }}" + loop_control: + index_var: _index + register: r_service + until: r_service is success + retries: "{{ openshift_cnv_retries }}" + delay: "{{ openshift_cnv_delay }}" + +- name: Create a service for subdomain lab + vars: + _definition: | + apiVersion: v1 + kind: Service + metadata: + name: "lab" + namespace: "{{ openshift_cnv_namespace }}" + spec: + clusterIP: None + ipFamilies: + - IPv4 + ports: + - name: foo + protocol: TCP + port: 1234 + targetPort: 1234 + selector: + kubevirt.io: virt-launcher + kubernetes.core.k8s: + definition: "{{ _definition }}" + register: r_service_lab + until: r_service_lab is success + retries: "{{ openshift_cnv_retries }}" + delay: "{{ openshift_cnv_delay }}" + when: "{{ openshift_cnv_add_lab_subdomain | default(True) }}" + + +- name: Create services for the instances + loop: "{{ _instance.services|default([]) }}" + loop_control: + loop_var: service + ansible.builtin.include_tasks: create_service.yaml diff --git a/roles/cnv_instances/tasks/main.yml b/roles/cnv_instances/tasks/main.yml new file mode 100644 index 0000000..23308e3 --- /dev/null +++ b/roles/cnv_instances/tasks/main.yml @@ -0,0 +1,11 @@ +--- +# -------------------------------------------------- +# Do not modify this file +# -------------------------------------------------- +- name: Running workload provision tasks + when: ACTION == "provision" + ansible.builtin.include_tasks: workload.yml + +- name: Running workload removal tasks + when: ACTION == "destroy" + ansible.builtin.include_tasks: remove_workload.yml diff --git a/roles/cnv_instances/tasks/remove_workload.yml b/roles/cnv_instances/tasks/remove_workload.yml new file mode 100644 index 0000000..37c8895 --- /dev/null +++ b/roles/cnv_instances/tasks/remove_workload.yml @@ -0,0 +1,8 @@ +--- +# ------------------------------------------ +# Implement your workload removal tasks here +# ------------------------------------------ + +- name: Deprovision your workload + ansible.builtin.debug: + msg: "Nothing to deprovision for example workload." diff --git a/roles/cnv_instances/tasks/workload.yml b/roles/cnv_instances/tasks/workload.yml new file mode 100644 index 0000000..11b60b5 --- /dev/null +++ b/roles/cnv_instances/tasks/workload.yml @@ -0,0 +1,33 @@ +--- +# ------------------------------------------------------------------------- +# To Do: Implement your workload deployment tasks here +# ------------------------------------------------------------------------- + +- set_fact: + r_openshift_cnv_instances: [] + +- name: Create instances, services and routes + module_defaults: + group/k8s: + host: "{{ sandbox_openshift_api_url }}" + api_key: "{{ sandbox_openshift_api_key }}" + validate_certs: false + block: + - name: Create Instances + loop: "{{ instances|default([]) }}" + loop_control: + loop_var: _instance + ansible.builtin.include_tasks: create_instance.yaml + + + - name: Create services for the nodes + ansible.builtin.include_tasks: create_services.yaml + loop: "{{ instances|default([]) }}" + loop_control: + loop_var: _instance + + - name: Create routes for the nodes + ansible.builtin.include_tasks: create_routes.yaml + loop: "{{ instances|default([]) }}" + loop_control: + loop_var: _instance diff --git a/roles/cnv_instances/vars/main.yml b/roles/cnv_instances/vars/main.yml new file mode 100644 index 0000000..0beb869 --- /dev/null +++ b/roles/cnv_instances/vars/main.yml @@ -0,0 +1,6 @@ +--- +# Any internal variables should be defined here. +# Variable names must start with a single underscore (_) followed by the workload role name +# Variable values should be empty to indicate that they will be set during the workload run. +# Add comments for documentation if so required. +_cnv_instances_internal: []