diff --git a/control-plane/README.md b/control-plane/README.md index 1237b970..0f2e04aa 100644 --- a/control-plane/README.md +++ b/control-plane/README.md @@ -10,8 +10,7 @@ Contains roles for deploying the metal-control-plane. | Port | Protocol | Service Name | Description | | ----- | -------- | ------------- | ----------------------------- | - | 4150 | TCP | nsqd | nsq Daemon (HTTPS) | - | 4161 | TCP | nsq-lookupd | nsqlookup Damon (HTTP) | + | 4150 | TCP | nsqd | nsq Daemon (TLS) | | 5222 | TCP | metal-console | Console forwarding (SSH) | | 50051 | TCP | metal-api | metal-api gRPC API (protobuf) | @@ -27,7 +26,6 @@ The `control-plane-defaults` folder contains defaults that are used by multiple | metal_control_plane_namespace | | The target namespace of all deployed kubernetes resources of the metal-control-plane | | metal_control_plane_image_pull_policy | | Global value for an ImagePullPolicy that will be used for Kubernetes entities | - ## Roles | Role Name | Description | diff --git a/control-plane/roles/gardener-monitoring-certs/README.md b/control-plane/roles/gardener-monitoring-certs/README.md new file mode 100644 index 00000000..120b7aed --- /dev/null +++ b/control-plane/roles/gardener-monitoring-certs/README.md @@ -0,0 +1,19 @@ +# gardener-monitoring-certs + +Deploys a control plane certificate to the soil and the seeds in order to provide monitoring certificates for the Grafanas. This role can only be executed when seed api servers are reachable. + +Please refer to the metal-stack gardener integration in our [documentation](https://docs.metal-stack.io/stable/overview/kubernetes/). + +Check out the Gardener project for further documentation on [gardener.cloud](https://gardener.cloud/). + +## Variables + +This role mainly tries to inherit variables from the `gardener` role, so it does not require many own variables. + +| Name | Mandatory | Description | +| ----------------------------------------- | --------- | ----------------------------------------------------------------------------------------- | +| gardener_seeds_certificate_cluster_issuer | yes | The issuer used for deploying a certificate for the Gardener monitoring ingresses for TLS | +| gardener_seeds_dns_domain | | The DNS domain used by the seeds | +| gardener_seeds_shooted_seeds | | A list of definitions for shooted seeds | +| gardener_seeds_soil_name | | The name of the initial `Seed` (used for spinning up shooted seeds) | +| gardener_seeds_virtual_garden_kubeconfig | | The kubeconfig for the kube-apiserver of the virtual garden | diff --git a/control-plane/roles/gardener-monitoring-certs/defaults/main/main.yaml b/control-plane/roles/gardener-monitoring-certs/defaults/main/main.yaml new file mode 100644 index 00000000..9cda4905 --- /dev/null +++ b/control-plane/roles/gardener-monitoring-certs/defaults/main/main.yaml @@ -0,0 +1,9 @@ +--- +gardener_seeds_certificate_cluster_issuer: +gardener_seeds_dns_domain: "{{ gardener_dns_domain }}" + +gardener_seeds_shooted_seeds: "{{ gardener_shooted_seeds }}" + +gardener_seeds_soil_name: "{{ metal_control_plane_stage_name }}" + +gardener_seeds_virtual_garden_kubeconfig: "{{ lookup('k8s', api_version='v1', kind='Secret', namespace='garden', resource_name='garden-kubeconfig-for-admin').get('data', {}).get('kubeconfig') | b64decode }}" diff --git a/control-plane/roles/gardener-monitoring-certs/tasks/deploy_cert.yaml b/control-plane/roles/gardener-monitoring-certs/tasks/deploy_cert.yaml new file mode 100644 index 00000000..6eb113df --- /dev/null +++ b/control-plane/roles/gardener-monitoring-certs/tasks/deploy_cert.yaml @@ -0,0 +1,43 @@ +--- +- name: Get seed kubeconfig + copy: + dest: "/tmp/kubeconfig.{{ gardener_shooted_seed.name }}" + content: "{{ lookup('k8s', kubeconfig='/tmp/kubeconfig.garden', api_version='v1', namespace='garden', kind='Secret', resource_name=gardener_shooted_seed.name+'.kubeconfig').get('data', {}).get('kubeconfig') | b64decode }}" + +- name: Add seed ingress certificate + k8s: + definition: + apiVersion: cert.gardener.cloud/v1alpha1 + kind: Certificate + metadata: + name: seed-ingress + namespace: garden + spec: + commonName: "*.ingress.{{ gardener_shooted_seed.name }}.{{ gardener_seeds_soil_name }}.{{ gardener_seeds_dns_domain }}" + issuerRef: + name: gardener + secretRef: + name: seed-ingress-certificate + namespace: garden + kubeconfig: "/tmp/kubeconfig.{{ gardener_shooted_seed.name }}" + +- name: Wait until ingress secret is ready + command: echo + changed_when: false + retries: 60 + delay: 10 + until: + - lookup('k8s', kubeconfig='/tmp/kubeconfig.'+gardener_shooted_seed.name, api_version='v1', namespace='garden', kind='Secret', resource_name='seed-ingress-certificate') + +- name: Prepare seed ingress certificate secret + k8s: + definition: + apiVersion: v1 + kind: Secret + metadata: + labels: + gardener.cloud/role: controlplane-cert + name: seed-ingress-certificate + namespace: garden + type: kubernetes.io/tls + kubeconfig: "/tmp/kubeconfig.{{ gardener_shooted_seed.name }}" diff --git a/control-plane/roles/gardener-monitoring-certs/tasks/main.yaml b/control-plane/roles/gardener-monitoring-certs/tasks/main.yaml new file mode 100644 index 00000000..efb57ffc --- /dev/null +++ b/control-plane/roles/gardener-monitoring-certs/tasks/main.yaml @@ -0,0 +1,51 @@ +--- +- name: Add ingress certificate for shooted seeds + k8s: + definition: + apiVersion: cert-manager.io/v1 + kind: Certificate + metadata: + name: seed-ingress + namespace: garden + labels: + use-clouddns-solver: "true" + spec: + dnsNames: + - "*.{{ gardener_seeds_dns_domain }}" + issuerRef: + group: cert-manager.io + kind: ClusterIssuer + name: "{{ gardener_seeds_certificate_cluster_issuer }}" + secretName: seed-ingress-certificate + +- name: Wait until ingress secret for shooted seeds is ready + command: echo + changed_when: false + retries: 60 + delay: 10 + until: + - lookup('k8s', api_version='v1', namespace='garden', kind='Secret', resource_name='seed-ingress-certificate') + +- name: Prepare seed ingress certificate secret for shooted seeds + k8s: + definition: + apiVersion: v1 + kind: Secret + metadata: + labels: + gardener.cloud/role: controlplane-cert + name: seed-ingress-certificate + namespace: garden + type: kubernetes.io/tls + +- name: Write virtual garden kubeconfig + copy: + dest: "/tmp/kubeconfig.garden" + content: "{{ gardener_seeds_virtual_garden_kubeconfig }}" + +- name: Loop over Gardener seeds + include_tasks: deploy_cert.yaml + loop: "{{ gardener_seeds_shooted_seeds }}" + loop_control: + loop_var: gardener_shooted_seed + label: "{{ gardener_shooted_seed.name }}" diff --git a/control-plane/roles/gardener/README.md b/control-plane/roles/gardener/README.md index 890bf888..f832f5ae 100644 --- a/control-plane/roles/gardener/README.md +++ b/control-plane/roles/gardener/README.md @@ -95,6 +95,7 @@ This includes the metal-stack extension provider called [gardener-extension-prov | gardener_extension_provider_metal_image_pull_policy | | Sets the image pull policy for components deployed through this extension controller. | | gardener_extension_provider_metal_image_pull_secret | | Provide image pull secrets for deployed containers | | gardener_cert_management_issuer_private_key | | The Let's Encrypt private key used by the cert-management extension controller to setup signed certificates | +| gardener_extension_dns_external_controller_registration_url | | Allows to define a URL to the controller registration yaml | ### Certificates diff --git a/control-plane/roles/gardener/defaults/main/extensions.yaml b/control-plane/roles/gardener/defaults/main/extensions.yaml index 0feac1b5..6b3cb0c7 100644 --- a/control-plane/roles/gardener/defaults/main/extensions.yaml +++ b/control-plane/roles/gardener/defaults/main/extensions.yaml @@ -1,7 +1,6 @@ --- gardener_os_controller_repo_ref: "{{ gardener_os_controller_image_tag }}" -# TODO: the ref to the official controller can be used as soon as we are compatible with g/g 1.59 -gardener_networking_cilium_repo_ref: "metal-stack/gardener-extension-networking-cilium/{{ gardener_networking_cilium_image_tag }}" +gardener_networking_cilium_repo_ref: "gardener/gardener-extension-networking-cilium/{{ gardener_networking_cilium_image_tag }}" gardener_metal_admission_replicas: 1 gardener_metal_admission_vpa: true @@ -58,3 +57,5 @@ gardener_extension_provider_metal_image_pull_secret: # ... gardener_cert_management_issuer_private_key: "" + +gardener_extension_dns_external_controller_registration_url: diff --git a/control-plane/roles/gardener/defaults/main/gardener.yaml b/control-plane/roles/gardener/defaults/main/gardener.yaml index 39e98e9e..ef91737c 100644 --- a/control-plane/roles/gardener/defaults/main/gardener.yaml +++ b/control-plane/roles/gardener/defaults/main/gardener.yaml @@ -40,7 +40,7 @@ gardener_soil_kubeconfig_file_path: "{{ lookup('env', 'KUBECONFIG') }}" gardener_soil_vertical_pod_autoscaler_enabled: false gardener_soil_project_owner_name: admin -gardener_gardenlet_shoot_concurrent_syncs: 5 +gardener_gardenlet_shoot_concurrent_syncs: 20 gardener_gardenlet_shoot_reconcile_in_maintenance_only: false gardener_gardenlet_shoot_respect_sync_period_overwrite: true diff --git a/control-plane/roles/gardener/templates/dns/controller-deployment.yaml b/control-plane/roles/gardener/templates/dns/controller-deployment.yaml index b566177d..bcddbe1c 100644 --- a/control-plane/roles/gardener/templates/dns/controller-deployment.yaml +++ b/control-plane/roles/gardener/templates/dns/controller-deployment.yaml @@ -5,9 +5,11 @@ metadata: name: dns-external type: helm providerConfig: - # this is generated by hand because a regression! - # see our fork github.com/metal-stack/external-dns-management@v0.12.6-crd-fix - chart: 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 +{% if gardener_extension_dns_external_controller_registration_url %} + chart: "{{ (lookup('url', gardener_extension_dns_external_controller_registration_url, split_lines=False) | from_yaml_all | list)[0].providerConfig.chart }}" +{% else %} + chart: "{{ (lookup('url', 'https://raw.githubusercontent.com/gardener/external-dns-management/' + gardener_external_dns_image_tag + '/examples/controller-registration.yaml', split_lines=False) | from_yaml_all | list)[0].providerConfig.chart }}" +{% endif %} values: createCRDs: false image: diff --git a/control-plane/roles/gardener/templates/gardenlet-values.j2 b/control-plane/roles/gardener/templates/gardenlet-values.j2 index 9b570fca..86f63e15 100644 --- a/control-plane/roles/gardener/templates/gardenlet-values.j2 +++ b/control-plane/roles/gardener/templates/gardenlet-values.j2 @@ -1,78 +1,84 @@ -global: - gardenlet: - image: - repository: {{ gardener_gardenlet_image_name }} - tag: {{ gardener_gardenlet_image_tag }} - pullPolicy: {{ metal_control_plane_image_pull_policy }} - config: - gardenClientConnection: - gardenClusterAddress: {{ (lookup('file', gardener_kube_apiserver_kubeconfig_path) | from_yaml).get("clusters")[0]["cluster"]["server"] }} - gardenClusterCACert: {{ (lookup('file', gardener_kube_apiserver_kubeconfig_path) | from_yaml).get("clusters")[0]["cluster"]["certificate-authority-data"] }} - kubeconfig: | - {{ lookup('file', gardener_kube_apiserver_kubeconfig_path) | indent(width=10, first=false) }} +image: + repository: {{ gardener_gardenlet_image_name }} + tag: {{ gardener_gardenlet_image_tag }} + pullPolicy: {{ metal_control_plane_image_pull_policy }} +config: + gardenClientConnection: + gardenClusterAddress: {{ (lookup('file', gardener_kube_apiserver_kubeconfig_path) | from_yaml).get("clusters")[0]["cluster"]["server"] }} + gardenClusterCACert: {{ (lookup('file', gardener_kube_apiserver_kubeconfig_path) | from_yaml).get("clusters")[0]["cluster"]["certificate-authority-data"] }} + kubeconfig: | + {{ lookup('file', gardener_kube_apiserver_kubeconfig_path) | indent(width=6, first=false) }} - controllers: - backupEntry: - deletionGracePeriodHours: 72 - deletionGracePeriodShootPurposes: - - evaluation - - infrastructure - - production - shoot: - concurrentSyncs: {{ gardener_gardenlet_shoot_concurrent_syncs }} - reconcileInMaintenanceOnly: {{ gardener_gardenlet_shoot_reconcile_in_maintenance_only }} - # allow setting shoot ignore annotation: - respectSyncPeriodOverwrite: {{ gardener_gardenlet_shoot_respect_sync_period_overwrite }} + controllers: + backupEntry: + deletionGracePeriodHours: 72 + deletionGracePeriodShootPurposes: + - evaluation + - infrastructure + - production + shoot: + concurrentSyncs: {{ gardener_gardenlet_shoot_concurrent_syncs }} + reconcileInMaintenanceOnly: {{ gardener_gardenlet_shoot_reconcile_in_maintenance_only }} + # allow setting shoot ignore annotation: + respectSyncPeriodOverwrite: {{ gardener_gardenlet_shoot_respect_sync_period_overwrite }} - seedConfig: - apiVersion: core.gardener.cloud/v1beta1 - kind: Seed - metadata: - name: {{ gardener_soil_name }} - labels: - name: {{ gardener_soil_name }} - spec: - provider: - type: {{ metal_control_plane_host_provider }} - region: local + seedConfig: + apiVersion: core.gardener.cloud/v1beta1 + kind: Seed + metadata: + name: {{ gardener_soil_name }} + labels: + name: {{ gardener_soil_name }} + spec: + provider: + type: {{ metal_control_plane_host_provider }} + region: local + secretRef: + name: gardener-seed-kubeconfig + namespace: garden + dns: + provider: secretRef: - name: gardener-seed-kubeconfig + name: {{ lookup('k8s', kubeconfig=gardener_kube_apiserver_kubeconfig_path, api_version='v1', kind='Secret', namespace='garden', label_selector='gardener.cloud/role=internal-domain').get('metadata', {}).get('name') }} namespace: garden - dns: - ingressDomain: {{ gardener_dns_domain }} - networks: - nodes: "{{ _gardener_gardenlet_node_cidr }}" - pods: "{{ _gardener_gardenlet_pod_cidr }}" - services: "{{ _gardener_gardenlet_service_cidr }}" - backup: {{ gardener_backup_infrastructure | to_json }} - blockCIDRs: [] - settings: - excessCapacityReservation: - enabled: false - scheduling: - visible: false - shootDNS: - enabled: true - verticalPodAutoscaler: - enabled: {{ gardener_soil_vertical_pod_autoscaler_enabled }} - taints: - - key: seed.gardener.cloud/protected - - key: seed.gardener.cloud/invisible - - key: seed.gardener.cloud/disable-capacity-reservation + type: {{ gardener_dns_provider }} + ingress: + controller: + kind: nginx + domain: {{ gardener_dns_domain }} + networks: + nodes: "{{ _gardener_gardenlet_node_cidr }}" + pods: "{{ _gardener_gardenlet_pod_cidr }}" + services: "{{ _gardener_gardenlet_service_cidr }}" + backup: {{ gardener_backup_infrastructure | to_json }} + blockCIDRs: [] + settings: + excessCapacityReservation: + enabled: false + scheduling: + visible: false + shootDNS: + enabled: true + verticalPodAutoscaler: + enabled: {{ gardener_soil_vertical_pod_autoscaler_enabled }} + taints: + - key: seed.gardener.cloud/protected + - key: seed.gardener.cloud/invisible + - key: seed.gardener.cloud/disable-capacity-reservation - featureGates: - ManagedIstio: true - HVPA: false - HVPAForShootedSeed: false + featureGates: + ManagedIstio: true + HVPA: false + HVPAForShootedSeed: false - vpa: {{ gardener_soil_vertical_pod_autoscaler_enabled }} +vpa: {{ gardener_soil_vertical_pod_autoscaler_enabled }} {% if gardener_image_vector_overwrite %} - imageVectorOverwrite: | - images: - {{ gardener_image_vector_overwrite | to_yaml | indent(width=6, first=false) }} +imageVectorOverwrite: | + images: + {{ gardener_image_vector_overwrite | to_yaml | indent(width=4, first=false) }} {% endif %} {% if gardener_component_image_vector_overwrite %} - componentImageVectorOverwrites: | - {{ gardener_component_image_vector_overwrite | to_yaml | indent(width=6, first=false) }} +componentImageVectorOverwrites: | + {{ gardener_component_image_vector_overwrite | to_yaml | indent(width=4, first=false) }} {% endif %} diff --git a/control-plane/roles/postgres-backup-restore/tasks/main.yml b/control-plane/roles/postgres-backup-restore/tasks/main.yml index 922449e4..5498d668 100644 --- a/control-plane/roles/postgres-backup-restore/tasks/main.yml +++ b/control-plane/roles/postgres-backup-restore/tasks/main.yml @@ -12,7 +12,19 @@ - postgres_backup_restore_sidecar_image_name is defined - postgres_backup_restore_sidecar_image_tag is defined +- name: Migration to separate backup volume (since pre- and post-cmds) + k8s: + definition: + apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: "{{ postgres_name }}" + namespace: "{{ postgres_namespace }}" + state: absent + when: "(lookup('k8s', api_version='apps/v1', kind='StatefulSet', resource_name=postgres_name, namespace=postgres_namespace) | default({}, true)).get('spec', {}).get('volumeClaimTemplates', []) | length == 1" + - name: Deploy postgres (backup-restore) k8s: definition: "{{ lookup('template', 'postgres.yaml') }}" namespace: "{{ postgres_namespace }}" + apply: yes diff --git a/control-plane/roles/postgres-backup-restore/templates/postgres.yaml b/control-plane/roles/postgres-backup-restore/templates/postgres.yaml index 62d5dca2..566a8325 100644 --- a/control-plane/roles/postgres-backup-restore/templates/postgres.yaml +++ b/control-plane/roles/postgres-backup-restore/templates/postgres.yaml @@ -45,18 +45,8 @@ spec: image: {{ postgres_image_name }}:{{ postgres_image_tag }} imagePullPolicy: {{ postgres_image_pull_policy }} command: - - tini - - -- - args: - - sh - - -c - - backup-restore-sidecar wait && docker-entrypoint.sh postgres - {% if postgres_shared_libraries_preload %}-c shared_preload_libraries={{ postgres_shared_libraries_preload | join(',') }}{% endif %} - {% if postgres_maintenance_work_mem %}-c maintenance_work_mem={{ postgres_maintenance_work_mem }}{% endif %} - {% if postgres_shared_buffers %}-c shared_buffers={{ postgres_shared_buffers }}{% endif %} - {% if postgres_effective_cache_size %}-c effective_cache_size={{ postgres_effective_cache_size }}{% endif %} - {% if postgres_work_mem %}-c work_mem={{ postgres_work_mem }}{% endif %} - -c max_connections={{ postgres_max_connections }} + - backup-restore-sidecar + - wait ports: - containerPort: 5432 env: @@ -80,6 +70,43 @@ spec: secretKeyRef: key: POSTGRES_DATA name: {{ postgres_name }} + livenessProbe: + exec: + command: + - /bin/sh + - -c + - exec + - pg_isready + - -U + - {{ postgres_user }} + - -h + - 127.0.0.1 + - -p + - "5432" + failureThreshold: 6 + initialDelaySeconds: 30 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + name: postgres + ports: + - containerPort: 5432 + readinessProbe: + exec: + command: + - /bin/sh + - -c + - exec + - pg_isready + - -U + - {{ postgres_user }} + - -h + - 127.0.0.1 + - -p + - "5432" + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 resources: {{ postgres_resources | to_json }} volumeMounts: - name: {{ postgres_name }} @@ -89,21 +116,15 @@ spec: mountPath: /usr/local/bin/backup-restore-sidecar - name: backup-restore-sidecar-config mountPath: /etc/backup-restore-sidecar - - name: bin-provision - subPath: tini - mountPath: /usr/local/bin/tini - name: backup-restore-sidecar image: {{ postgres_image_name }}:{{ postgres_image_tag }} imagePullPolicy: {{ postgres_image_pull_policy }} command: - - tini - - -- - args: - - sh - - -c - - mkdir -p /data/postgres && backup-restore-sidecar start + - backup-restore-sidecar + - start ports: - - containerPort: 2112 + - containerPort: 8000 + name: grpc env: - name: BACKUP_RESTORE_SIDECAR_POSTGRES_PASSWORD valueFrom: @@ -137,14 +158,13 @@ spec: volumeMounts: - name: {{ postgres_name }} mountPath: /data + - mountPath: /backup + name: backup - name: bin-provision subPath: backup-restore-sidecar mountPath: /usr/local/bin/backup-restore-sidecar - name: backup-restore-sidecar-config mountPath: /etc/backup-restore-sidecar - - name: bin-provision - subPath: tini - mountPath: /usr/local/bin/tini {% if postgres_backup_restore_sidecar_provider == "gcp" %} - name: gcp-credentials mountPath: /gcp/credentials @@ -157,7 +177,6 @@ spec: command: - cp - /backup-restore-sidecar - - /sbin/tini - /bin-provision volumeMounts: - name: bin-provision @@ -166,6 +185,9 @@ spec: - name: {{ postgres_name }} persistentVolumeClaim: claimName: {{ postgres_name }} + - name: backup + persistentVolumeClaim: + claimName: backup - name: backup-restore-sidecar-config configMap: name: backup-restore-sidecar-config-{{ postgres_name }} @@ -190,6 +212,17 @@ spec: storage: {{ postgres_storage_size }} {% if postgres_storage_class %} storageClassName: {{ postgres_storage_class }} +{% endif %} + - metadata: + name: backup + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ postgres_storage_size }} +{% if postgres_storage_class %} + storageClassName: {{ postgres_storage_class }} {% endif %} --- apiVersion: v1 @@ -204,9 +237,12 @@ data: backup-provider: {{ postgres_backup_restore_sidecar_provider }} backup-cron-schedule: "{{ postgres_backup_restore_sidecar_backup_cron_schedule }}" object-prefix: {{ postgres_backup_restore_sidecar_object_prefix }} + compression-method: targz {% if postgres_backup_restore_sidecar_object_max_keep %} object-max-keep: {{ postgres_backup_restore_sidecar_object_max_keep }} {% endif %} + post-exec-cmds: + - docker-entrypoint.sh postgres {% if postgres_shared_libraries_preload %} -c shared_preload_libraries={{ postgres_shared_libraries_preload | join(',') }}{% endif %}{% if postgres_maintenance_work_mem %} -c maintenance_work_mem={{ postgres_maintenance_work_mem }}{% endif %}{% if postgres_shared_buffers %} -c shared_buffers={{ postgres_shared_buffers }}{% endif %}{% if postgres_effective_cache_size %} -c effective_cache_size={{ postgres_effective_cache_size }}{% endif %}{% if postgres_work_mem %} -c work_mem={{ postgres_work_mem }}{% endif %} -c max_connections={{ postgres_max_connections }} --- apiVersion: v1 kind: Secret diff --git a/control-plane/roles/rethinkdb-backup-restore/tasks/main.yml b/control-plane/roles/rethinkdb-backup-restore/tasks/main.yml index 9ec845ae..cfb05b5f 100644 --- a/control-plane/roles/rethinkdb-backup-restore/tasks/main.yml +++ b/control-plane/roles/rethinkdb-backup-restore/tasks/main.yml @@ -19,7 +19,19 @@ that: - rethinkdb_backup_restore_sidecar_image_tag is defined +- name: Migration to separate backup volume (since pre- and post-cmds) + k8s: + definition: + apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: "{{ rethinkdb_name }}" + namespace: "{{ rethinkdb_namespace }}" + state: absent + when: "(lookup('k8s', api_version='apps/v1', kind='StatefulSet', resource_name=rethinkdb_name, namespace=rethinkdb_namespace) | default({}, true)).get('spec', {}).get('volumeClaimTemplates', []) | length == 1" + - name: Deploy rethinkdb (backup-restore) k8s: definition: "{{ lookup('template', 'rethinkdb.yaml') }}" namespace: "{{ rethinkdb_namespace }}" + apply: yes diff --git a/control-plane/roles/rethinkdb-backup-restore/templates/rethinkdb.yaml b/control-plane/roles/rethinkdb-backup-restore/templates/rethinkdb.yaml index 3b8cb289..035a4289 100644 --- a/control-plane/roles/rethinkdb-backup-restore/templates/rethinkdb.yaml +++ b/control-plane/roles/rethinkdb-backup-restore/templates/rethinkdb.yaml @@ -37,13 +37,8 @@ spec: image: {{ rethinkdb_image_name }}:{{ rethinkdb_image_tag }} imagePullPolicy: {{ rethinkdb_image_pull_policy }} command: - - tini - - -- - args: - - sh - - -c - # IMPORTANT: the --directory needs to point to the exact sidecar data dir, otherwise the database will be restored to the wrong location - - backup-restore-sidecar wait && rethinkdb --bind all --directory /data/rethinkdb --initial-password ${RETHINKDB_PASSWORD} + - backup-restore-sidecar + - wait env: - name: RETHINKDB_PASSWORD valueFrom: @@ -62,21 +57,15 @@ spec: mountPath: /usr/local/bin/backup-restore-sidecar - name: backup-restore-sidecar-config mountPath: /etc/backup-restore-sidecar - - name: bin-provision - subPath: tini - mountPath: /usr/local/bin/tini - name: backup-restore-sidecar image: {{ rethinkdb_image_name }}:{{ rethinkdb_image_tag }} imagePullPolicy: {{ rethinkdb_image_pull_policy }} command: - - tini - - -- - args: - - sh - - -c - - mkdir -p /data/rethinkdb && backup-restore-sidecar start + - backup-restore-sidecar + - start ports: - - containerPort: 2112 + - containerPort: 8000 + name: grpc env: {% if rethinkdb_backup_restore_sidecar_provider == "gcp" %} - name: BACKUP_RESTORE_SIDECAR_GCP_PROJECT @@ -100,6 +89,8 @@ spec: volumeMounts: - mountPath: /data name: {{ rethinkdb_name }} + - mountPath: /backup + name: backup - name: rethinkdb-credentials mountPath: /rethinkdb-secret - name: backup-restore-sidecar-config @@ -107,9 +98,6 @@ spec: - name: bin-provision subPath: backup-restore-sidecar mountPath: /usr/local/bin/backup-restore-sidecar - - name: bin-provision - subPath: tini - mountPath: /usr/local/bin/tini - name: bin-provision subPath: rethinkdb-dump mountPath: /usr/local/bin/rethinkdb-dump @@ -128,7 +116,6 @@ spec: command: - cp - /backup-restore-sidecar - - /ubuntu/tini - /rethinkdb/rethinkdb-dump - /rethinkdb/rethinkdb-restore - /bin-provision @@ -139,6 +126,9 @@ spec: - name: {{ rethinkdb_name }} persistentVolumeClaim: claimName: {{ rethinkdb_name }} + - name: backup + persistentVolumeClaim: + claimName: backup - name: rethinkdb-credentials secret: secretName: {{ rethinkdb_name }} @@ -169,6 +159,17 @@ spec: storage: {{ rethinkdb_storage_size }} {% if rethinkdb_storage_class %} storageClassName: {{ rethinkdb_storage_class }} +{% endif %} + - metadata: + name: backup + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ rethinkdb_storage_size }} +{% if rethinkdb_storage_class %} + storageClassName: {{ rethinkdb_storage_class }} {% endif %} --- apiVersion: v1 @@ -184,6 +185,9 @@ data: rethinkdb-passwordfile: /rethinkdb-secret/rethinkdb-password.txt backup-cron-schedule: "{{ rethinkdb_backup_restore_sidecar_backup_cron_schedule }}" object-prefix: rethinkdb-{{ metal_control_plane_stage_name }} + post-exec-cmds: + # IMPORTANT: the --directory needs to point to the exact sidecar data dir, otherwise the database will be restored to the wrong location + - rethinkdb --bind all --directory /data/rethinkdb --initial-password ${RETHINKDB_PASSWORD} {% if rethinkdb_backup_restore_sidecar_object_max_keep %} object-max-keep: {{ rethinkdb_backup_restore_sidecar_object_max_keep }} {% endif %} diff --git a/partition/roles/leaf/templates/frr.conf.j2 b/partition/roles/leaf/templates/frr.conf.j2 index f59216f2..384314af 100644 --- a/partition/roles/leaf/templates/frr.conf.j2 +++ b/partition/roles/leaf/templates/frr.conf.j2 @@ -33,7 +33,9 @@ router bgp {{ asn }} route-map LOOPBACKS permit 10 match interface lo ! +{% if metal_partition_mgmt_gateway %} ip route 0.0.0.0/0 {{ metal_partition_mgmt_gateway }} nexthop-vrf mgmt ! +{% endif %} line vty ! diff --git a/partition/roles/metal-bmc/README.md b/partition/roles/metal-bmc/README.md index f1fe8c0e..7b31a738 100644 --- a/partition/roles/metal-bmc/README.md +++ b/partition/roles/metal-bmc/README.md @@ -8,21 +8,24 @@ This role uses variables from [partition-defaults](/partition). So, make sure yo You can look up all the default values of this role [here](defaults/main.yaml). -| Name | Mandatory | Description | -| ---------------------------- | --------- | ----------------------------------------------------------------------------------------------------- | -| metal_bmc_image_name | yes | Image version of the metal-bmc | -| metal_bmc_image_tag | yes | Image tag of the metal-bmc | -| metal_bmc_superuser | yes | Name of the BMC superuser | -| metal_bmc_superuser_pwd | yes | Password of the BMC superuser | -| metal_bmc_nsq_lookupd_addr | yes | The address to the nsq-lookupd that metal-bmc uses for discovering the NSQ of the metal control plane | -| metal_bmc_nsq_log_level | | The metal-core log level used on NSQ communication | -| metal_bmc_nsq_tls_enabled | | Enables tls encryption on NSQ traffic | -| metal_bmc_nsq_cert_dir | | Defines the path of the NSQ certificates | -| metal_bmc_nsqd_ca_cert | | The CA certificate that signed the NSQ client cert | -| metal_bmc_nsqd_client_cert | | The NSQ client certificate | -| metal_bmc_console_port | | The port where to listen for incoming metal-console connections | -| metal_bmc_console_ca_cert | yes | The CA certificate for the metal-console port as a string | -| metal_bmc_console_cert | yes | The certificate for metal-console port as a string | -| metal_bmc_console_key | yes | The key for the metal-console port as a string | -| metal_bmc_console_cert_owner | | user of the created certificate files | -| metal_bmc_console_cert_group | | group of the created certificate files | +| Name | Mandatory | Description | +| ------------------------------ | --------- | ---------------------------------------------------------------------------------------------- | +| metal_bmc_image_name | yes | Image version of the metal-bmc | +| metal_bmc_image_tag | yes | Image tag of the metal-bmc | +| metal_bmc_superuser | yes | Name of the BMC superuser | +| metal_bmc_superuser_pwd | yes | Password of the BMC superuser | +| metal_bmc_nsqd_addr | yes | The address to the nsqd that metal-bmc uses for discovering the NSQ of the metal control plane | +| metal_bmc_nsq_log_level | | The metal-core log level used on NSQ communication | +| metal_bmc_nsq_tls_enabled | | Enables tls encryption on NSQ traffic | +| metal_bmc_nsq_cert_dir | | Defines the path of the NSQ certificates | +| metal_bmc_nsqd_ca_cert | | The CA certificate that signed the NSQ client cert | +| metal_bmc_nsqd_client_cert | | The NSQ client certificate | +| metal_bmc_nsqd_client_cert_key | | The NSQ client certificate key | +| metal_bmc_console_port | | The port where to listen for incoming metal-console connections | +| metal_bmc_console_ca_cert | yes | The CA certificate for the metal-console port as a string | +| metal_bmc_console_cert | yes | The certificate for metal-console port as a string | +| metal_bmc_console_key | yes | The key for the metal-console port as a string | +| metal_bmc_console_cert_owner | | user of the created certificate files | +| metal_bmc_console_cert_group | | group of the created certificate files | +| metal_bmc_ignore_macs | | when fetching bmc reports from the dhcp lease file, the given macs are ignored | +| metal_bmc_allowed_cidrs | | when fetching bmc reports from the dhcp lease file, ips in the given cidrs are ignored | diff --git a/partition/roles/metal-bmc/defaults/main/main.yaml b/partition/roles/metal-bmc/defaults/main/main.yaml index 7c991d4b..13606d6f 100755 --- a/partition/roles/metal-bmc/defaults/main/main.yaml +++ b/partition/roles/metal-bmc/defaults/main/main.yaml @@ -1,15 +1,17 @@ --- metal_bmc_ignore_macs: [] +metal_bmc_allowed_cidrs: + - 0.0.0.0/0 metal_bmc_bmc_superuser: metal_bmc_bmc_superuser_pwd: - metal_bmc_nsq_log_level: info metal_bmc_nsq_tls_enabled: true metal_bmc_nsq_cert_dir: /certs/nsq metal_bmc_nsqd_ca_cert: metal_bmc_nsqd_client_cert: +metal_bmc_nsqd_client_cert_key: metal_bmc_console_port: 3333 metal_bmc_console_cert_owner: root diff --git a/partition/roles/metal-bmc/tasks/main.yaml b/partition/roles/metal-bmc/tasks/main.yaml index d6ab57bc..e32da2a6 100755 --- a/partition/roles/metal-bmc/tasks/main.yaml +++ b/partition/roles/metal-bmc/tasks/main.yaml @@ -7,12 +7,14 @@ fail_msg: "not all mandatory variables given, check role documentation" quiet: yes that: + - metal_bmc_nsqd_addr is defined - metal_bmc_image_tag is defined - metal_bmc_image_name is defined - metal_bmc_bmc_superuser is defined - metal_bmc_bmc_superuser_pwd is defined - not metal_bmc_nsq_tls_enabled or (metal_bmc_nsq_tls_enabled and metal_bmc_nsqd_ca_cert is not none) - not metal_bmc_nsq_tls_enabled or (metal_bmc_nsq_tls_enabled and metal_bmc_nsqd_client_cert is not none) + - not metal_bmc_nsq_tls_enabled or (metal_bmc_nsq_tls_enabled and metal_bmc_nsqd_client_cert_key is not none) - metal_bmc_console_ca_cert is not none - metal_bmc_console_cert is not none - metal_bmc_console_key is not none @@ -34,6 +36,8 @@ content: "{{ metal_bmc_nsqd_ca_cert }}" - filename: client.pem content: "{{ metal_bmc_nsqd_client_cert }}" + - filename: client-key.pem + content: "{{ metal_bmc_nsqd_client_cert_key }}" loop_control: label: "{{ item.filename }}" when: metal_bmc_nsq_tls_enabled @@ -88,12 +92,14 @@ METAL_BMC_METAL_API_URL: "{{ metal_partition_metal_api_protocol }}://{{ metal_partition_metal_api_addr }}:{{ metal_partition_metal_api_port }}{{ metal_partition_metal_api_basepath }}" METAL_BMC_METAL_API_HMAC_KEY: "{{ metal_partition_metal_api_hmac_edit_key }}" METAL_BMC_IGNORE_MACS: "{{ metal_bmc_ignore_macs | join(',') }}" + METAL_BMC_ALLOWED_CIDRS: "{{ metal_bmc_allowed_cidrs | join(',') }}" METAL_BMC_IPMI_USER: "{{ metal_bmc_bmc_superuser }}" METAL_BMC_IPMI_PASSWORD: "{{ metal_bmc_bmc_superuser_pwd }}" - METAL_BMC_MQ_ADDRESS: "{{ metal_bmc_nsq_lookupd_addr }}" + METAL_BMC_MQ_ADDRESS: "{{ metal_bmc_nsqd_addr }}" METAL_BMC_MQ_LOGLEVEL: "{{ metal_bmc_nsq_log_level }}" METAL_BMC_MQ_CA_CERT_FILE: "{{metal_bmc_nsq_cert_dir }}/ca.pem" METAL_BMC_MQ_CLIENT_CERT_FILE: "{{ metal_bmc_nsq_cert_dir }}/client.pem" + METAL_BMC_MQ_CLIENT_CERT_KEY_FILE: "{{ metal_bmc_nsq_cert_dir }}/client-key.pem" METAL_BMC_MACHINE_TOPIC: "{{ metal_partition_id }}-machine" METAL_BMC_CONSOLE_PORT: "{{ metal_bmc_console_port }}" METAL_BMC_CONSOLE_CA_CERT_FILE: "{{metal_bmc_console_cert_dir }}/ca.pem" diff --git a/partition/roles/metal-core/templates/metal-core-env.j2 b/partition/roles/metal-core/templates/metal-core-env.j2 index 3f70ca5c..9692093b 100644 --- a/partition/roles/metal-core/templates/metal-core-env.j2 +++ b/partition/roles/metal-core/templates/metal-core-env.j2 @@ -1,5 +1,4 @@ TZ: "{{ metal_partition_timezone }}" - METAL_CORE_LOOPBACK_IP: "{{ lo }}" METAL_CORE_ASN: "{{ asn }}" METAL_CORE_CIDR: "{{ metal_core_cidr }}" @@ -15,15 +14,13 @@ METAL_CORE_HMAC_KEY: "{{ metal_partition_metal_api_hmac_edit_key }}" METAL_CORE_LOG_LEVEL: "{{ metal_core_log_level }}" METAL_CORE_RECONFIGURE_SWITCH: "{{ metal_core_reconfigure_switch }}" METAL_CORE_RECONFIGURE_SWITCH_INTERVAL: "{{ metal_core_reconfigure_switch_interval }}" +{% if metal_partition_mgmt_gateway %} METAL_CORE_MANAGEMENT_GATEWAY: "{{ metal_partition_mgmt_gateway }}" +{% endif %} METAL_CORE_GRPC_ADDRESS: "{{ metal_core_grpc_address }}" METAL_CORE_GRPC_CA_CERT_FILE: "{{ metal_core_grpc_cert_dir }}/ca.pem" METAL_CORE_GRPC_CLIENT_CERT_FILE: "{{ metal_core_grpc_cert_dir }}/client.pem" METAL_CORE_GRPC_CLIENT_KEY_FILE: "{{ metal_core_grpc_cert_dir }}/client-key.pem" -METAL_CORE_ASN: "{{ asn }}" -METAL_CORE_RECONFIGURE_SWITCH: "{{ metal_core_reconfigure_switch }}" -METAL_CORE_RECONFIGURE_SWITCH_INTERVAL: "{{ metal_core_reconfigure_switch_interval }}" -METAL_CORE_MANAGEMENT_GATEWAY: "{{ metal_partition_mgmt_gateway }}" METAL_CORE_ADDITIONAL_BRIDGE_VIDS: "{{ metal_core_additional_bridge_vids | join(',') }}" METAL_CORE_ADDITIONAL_BRIDGE_PORTS: "{{ metal_core_additional_bridge_ports | join(',') }}" {% if metal_core_spine_uplinks is defined %} diff --git a/partition/roles/sonic/README.md b/partition/roles/sonic/README.md index 766111df..a083b47d 100644 --- a/partition/roles/sonic/README.md +++ b/partition/roles/sonic/README.md @@ -42,6 +42,7 @@ It depends on the `switch_facts` module from `ansible-common`, so make sure modu | sonic_vlans.ip | | The IP of the SVI of this VLAN. | | sonic_vlans.dhcp_servers | | DHCP servers to relay to. | | sonic_vlans.untagged_ports | | Array of untagged ports to bind to this VLAN. | +| sonic_vlans.tagged_ports | | Array of tagged ports to bind to this VLAN. | | sonic_vlans.vrf | | The VRF to bind the VLANs SVI to. | | sonic_vteps | | VTEPs to configure. If defined FRR will automatically advertise all VNIs. | | sonic_vteps.comment | | Description for the VTEP. | diff --git a/partition/roles/sonic/templates/metal.yaml.j2 b/partition/roles/sonic/templates/metal.yaml.j2 index 1b2a2fa0..fe86e054 100644 --- a/partition/roles/sonic/templates/metal.yaml.j2 +++ b/partition/roles/sonic/templates/metal.yaml.j2 @@ -27,8 +27,12 @@ LOOPBACK_INTERFACE: {% if sonic_mgmtif_ip is defined %} MGMT_INTERFACE: +{% if sonic_mgmtif_gateway is defined %} eth0|{{ sonic_mgmtif_ip }}: gwaddr: "{{ sonic_mgmtif_gateway }}" +{% else %} + eth0|{{ sonic_mgmtif_ip }}: {} +{% endif %} {% endif %} MGMT_VRF_CONFIG: @@ -96,6 +100,10 @@ VLAN_MEMBER: Vlan{{ vlan.id }}|{{ untagged_port }}: tagging_mode: untagged {% endfor %} + {% for tagged_port in vlan.tagged_ports|default([]) %} + Vlan{{ vlan.id }}|{{ tagged_port }}: + tagging_mode: tagged + {% endfor %} {% endfor %} {% endif %} {% endif %} diff --git a/partition/roles/systemd-networkd/README.md b/partition/roles/systemd-networkd/README.md index d3ef6f59..350cf53f 100644 --- a/partition/roles/systemd-networkd/README.md +++ b/partition/roles/systemd-networkd/README.md @@ -6,32 +6,44 @@ This role can deploy on bare metal machines with Debian or Almalinux. It depends ## Variables -| Name | Mandatory | Description | -| ---------------------------------------- | --------- | --------------------------------------------------------------------------------------------------- | -| systemd_networkd_mtu | | The MTU to use for interfaces. | -| systemd_networkd_vrfs | | An array of VRFs to be configured. | -| systemd_networkd_vrfs.name | | The name of the VRF. | -| systemd_networkd_vrfs.table | | The routing table id of the VRF. | -| systemd_networkd_nics | | An array of network interfaces to be configured. Mac or Name is mandatory. | -| systemd_networkd_nics.mac | | The MAC of the network interface. | -| systemd_networkd_nics.mtu | | The MTU to use for this interface. | -| systemd_networkd_nics.name | | The name this interface will be renamed to. | -| systemd_networkd_nics.dhcp | | Configure the interface addresses with DHCP. | -| systemd_networkd_nics.dhcpv4routemetrics | | The metric to apply to routes learned through DHCPv4. | -| systemd_networkd_nics.addresses | | array of IP addresses for the interfaces in CIDR notation. | -| systemd_networkd_nics.gateways | | array of Gateways IPs for the interfaces. | -| systemd_networkd_nics.vrf | | The VRF to bind this interface to. | -| systemd_networkd_nics.vxlans | | array of VXLANs to terminate on a physical interface. | -| systemd_networkd_vxlans | | VXLANs to terminate on a server. | -| systemd_networkd_vxlans.vtep.iface | | The VXLAN interface that should serve as VTEP. | -| systemd_networkd_vxlans.vtep.vni | | The network identifier of a VXLAN - should be unique within a BGP/EVPN-CLOS topology. | -| systemd_networkd_vxlans.vtep.ip | | The IP address of the tunnel endpoint (usually the loopback address when used with bgp unnumbered). | -| systemd_networkd_vxlans.vtep.mtu | | The MTU for the VXLAN interface. | -| systemd_networkd_vxlans.svi.iface | | The VLAN interface that should be attached to the VTEP. | -| systemd_networkd_vxlans.svi.vlanid | | The local VLAN ID. | -| systemd_networkd_vxlans.svi.vrf | | The VRF that should be used as master device for the VLAN interface. | -| systemd_networkd_vxlans.svi.address | | The IP address that should be configured at the VLAN interface. | -| systemd_networkd_vxlans.svi.mtu | | The MTU for the VLAN interface. | +| Name | Mandatory | Description | +| ------------------------------------------- | --------- | --------------------------------------------------------------------------------------------------- | +| systemd_networkd_mtu | | The MTU to use for interfaces. | +| systemd_networkd_vrfs | | An array of VRFs to be configured. | +| systemd_networkd_vrfs.name | | The name of the VRF. | +| systemd_networkd_vrfs.table | | The routing table id of the VRF. | +| systemd_networkd_nics | | An array of network interfaces to be configured. Mac or Name is mandatory. | +| systemd_networkd_nics.mac | | The MAC of the network interface. | +| systemd_networkd_nics.mtu | | The MTU to use for this interface. | +| systemd_networkd_nics.name | | The name this interface will be renamed to. | +| systemd_networkd_nics.type | | The type of this interface. | +| systemd_networkd_nics.dhcp | | Configure the interface addresses with DHCP. | +| systemd_networkd_nics.dhcpv4routemetrics | | The metric to apply to routes learned through DHCPv4. | +| systemd_networkd_nics.link_local_addressing | | Configure link local addressing. | +| systemd_networkd_nics.lldp | | Configure LLDP. | +| systemd_networkd_nics.emit_lldp | | Define whether to emit LLDP or not. | +| systemd_networkd_nics.ipv6_accept_ra | | Whether or not to accept IPv6 router advertisement on a link. | +| systemd_networkd_nics.ipv6_send_ra | | Whether or not to send IPv6 router advertisement on a link. | +| systemd_networkd_nics.addresses | | array of IP addresses for the interfaces in CIDR notation. | +| systemd_networkd_nics.gateways | | array of Gateways IPs for the interfaces. | +| systemd_networkd_nics.vrf | | The VRF to bind this interface to. | +| systemd_networkd_nics.vxlans | | array of VXLANs to terminate on a physical interface. | +| systemd_networkd_nics.vlans | | array of VLANs to attach to a network. | +| systemd_networkd_vxlans | | VXLANs to terminate on a server. | +| systemd_networkd_vxlans.vtep.iface | | The VXLAN interface that should serve as VTEP. | +| systemd_networkd_vxlans.vtep.vni | | The network identifier of a VXLAN - should be unique within a BGP/EVPN-CLOS topology. | +| systemd_networkd_vxlans.vtep.ip | | The IP address of the tunnel endpoint (usually the loopback address when used with bgp unnumbered). | +| systemd_networkd_vxlans.vtep.mtu | | The MTU for the VXLAN interface. | +| systemd_networkd_vxlans.svi.iface | | The VLAN interface that should be attached to the VTEP. | +| systemd_networkd_vxlans.svi.vlanid | | The local VLAN ID. | +| systemd_networkd_vxlans.svi.vrf | | The VRF that should be used as master device for the VLAN interface. | +| systemd_networkd_vxlans.svi.address | | The IP address that should be configured at the VLAN interface. | +| systemd_networkd_vxlans.svi.mtu | | The MTU for the VLAN interface. | +| systemd_networkd_vlans | | An array of VLANs to be configured. | +| systemd_networkd_vlans.name | | The name of this VLAN. | +| systemd_networkd_vlans.id | | The id of this VLAN. | +| systemd_networkd_vlans.mtu | | The MTU for this VLAN. | +| systemd_networkd_vlans.address | | The network address for this VLAN. | ## Examples diff --git a/partition/roles/systemd-networkd/defaults/main.yaml b/partition/roles/systemd-networkd/defaults/main.yaml index 6b631e41..8bdbf0e5 100644 --- a/partition/roles/systemd-networkd/defaults/main.yaml +++ b/partition/roles/systemd-networkd/defaults/main.yaml @@ -2,4 +2,5 @@ systemd_networkd_mtu: 9000 systemd_networkd_nics: [] systemd_networkd_vrfs: [] +systemd_networkd_vlans: [] systemd_networkd_vxlans: [] diff --git a/partition/roles/systemd-networkd/tasks/main.yaml b/partition/roles/systemd-networkd/tasks/main.yaml index 1169907c..84ff34f3 100644 --- a/partition/roles/systemd-networkd/tasks/main.yaml +++ b/partition/roles/systemd-networkd/tasks/main.yaml @@ -52,8 +52,26 @@ loop_control: index_var: i +- name: Render systemd-networkd vlan netdev config + template: + src: vlan.netdev.j2 + dest: "/etc/systemd/network/{{ i+71 }}-{{ item.name }}.netdev" + notify: restart systemd-networkd + loop: "{{ systemd_networkd_vlans }}" + loop_control: + index_var: i + +- name: Render systemd-networkd vlan network config + template: + src: vlan.network.j2 + dest: "/etc/systemd/network/{{ i+71 }}-{{ item.name }}.network" + notify: restart systemd-networkd + loop: "{{ systemd_networkd_vlans }}" + loop_control: + index_var: i + - include_tasks: vxlans.yaml - when: systemd_networkd_vxlans | length > 0 + when: systemd_networkd_vxlans - name: Flush handlers meta: flush_handlers diff --git a/partition/roles/systemd-networkd/templates/link.j2 b/partition/roles/systemd-networkd/templates/link.j2 index 6090d716..6b989c2d 100644 --- a/partition/roles/systemd-networkd/templates/link.j2 +++ b/partition/roles/systemd-networkd/templates/link.j2 @@ -1,6 +1,6 @@ [Match] {% if item.mac is defined %} -MACAddress={{ item.mac }} +PermanentMACAddress={{ item.mac }} {% else %} Name={{ item.name }} {% endif %} diff --git a/partition/roles/systemd-networkd/templates/network.j2 b/partition/roles/systemd-networkd/templates/network.j2 index 7e86426c..d3e218ff 100644 --- a/partition/roles/systemd-networkd/templates/network.j2 +++ b/partition/roles/systemd-networkd/templates/network.j2 @@ -4,16 +4,30 @@ MACAddress={{ item.mac }} {% else %} Name={{ item.name }} {% endif %} +{% if item.type is defined %} +Type={{ item.type }} +{% endif %} [Network] {% if item.dhcp is defined %} DHCP={{ item.dhcp }} {% endif %} -LinkLocalAddressing=ipv6 +LinkLocalAddressing={{ item.link_local_addressing | default('ipv6') }} +{% if item.lldp is defined %}LLDP={{ item.lldp }}{% endif %} + +{% if item.emit_lldp is defined %}EmitLLDP={{ item.emit_lldp }}{% endif %} + +{% if item.ipv6_accept_ra is defined %}IPv6AcceptRA={{ item.ipv6_accept_ra }}{% endif %} + +{% if item.ipv6_send_ra is defined %}IPv6SendRA={{ item.ipv6_send_ra }}{% endif %} + +{% for vlan in item.vlans | default([]) %} +VLAN={{ vlan }} +{% endfor %} {% if item.vrf is defined %} VRF={{ item.vrf }} {% endif %} -{% for vxlan in item.vxlans|default([]) %} +{% for vxlan in item.vxlans | default([]) %} VXLAN={{ vxlan }} {% endfor %} {% if item.dhcp is defined and item.dhcpv4routemetrics is defined %} @@ -21,12 +35,12 @@ VXLAN={{ vxlan }} [DHCPv4] RouteMetric={{ item.dhcpv4routemetrics }} {% endif %} -{% for address in item.addresses|default([]) %} +{% for address in item.addresses | default([]) %} [Address] Address={{ address }} {% endfor %} -{% for gateway in item.gateways|default([]) %} +{% for gateway in item.gateways | default([]) %} [Route] Gateway={{ gateway}} diff --git a/partition/roles/systemd-networkd/templates/vlan.netdev.j2 b/partition/roles/systemd-networkd/templates/vlan.netdev.j2 new file mode 100644 index 00000000..80010bb8 --- /dev/null +++ b/partition/roles/systemd-networkd/templates/vlan.netdev.j2 @@ -0,0 +1,6 @@ +[NetDev] +Name={{ item.name }} +Kind=vlan + +[VLAN] +Id={{ item.id }} diff --git a/partition/roles/systemd-networkd/templates/vlan.network.j2 b/partition/roles/systemd-networkd/templates/vlan.network.j2 new file mode 100644 index 00000000..4eb64f43 --- /dev/null +++ b/partition/roles/systemd-networkd/templates/vlan.network.j2 @@ -0,0 +1,9 @@ +[Match] +Name={{ item.name }} +Type=vlan + +[Link] +MTUBytes={{ item.mtu | default(systemd_networkd_mtu) }} + +[Network] +Address={{ item.address }} diff --git a/partition/roles/systemd-networkd/test/__init__.py b/partition/roles/systemd-networkd/test/__init__.py new file mode 100644 index 00000000..4795a238 --- /dev/null +++ b/partition/roles/systemd-networkd/test/__init__.py @@ -0,0 +1,6 @@ +import os + + +def read_template_file(name): + with open(os.path.join(os.path.dirname(__file__), "..", "templates", name), 'r') as f: + return f.read() diff --git a/partition/roles/systemd-networkd/test/template_test.py b/partition/roles/systemd-networkd/test/template_test.py new file mode 100644 index 00000000..1362f6b6 --- /dev/null +++ b/partition/roles/systemd-networkd/test/template_test.py @@ -0,0 +1,86 @@ +import unittest + +from test import read_template_file + +from ansible.template import Templar + + +defaults = dict( + systemd_networkd_mtu=9000, + systemd_networkd_nics=[], + systemd_networkd_vrfs=[], + systemd_networkd_vxlans=[], + systemd_networkd_vlans=[], +) + +class SystemdNetworkdTemplates(unittest.TestCase): + def test_template_inet_router(self): + self.maxDiff = None + + vars = defaults.copy() + + vars.update(item=dict( + mac="aa:aa:aa:aa:aa:aa", + type="ether", + link_local_addressing="no", + lldp="no", + emit_lldp="no", + ipv6_accept_ra="no", + ipv6_send_ra="no", + vlans=["vlan1", "vlanInternet"], + ),) + + templar = Templar(loader=None, variables=vars) + res = templar.template(read_template_file('network.j2')) + + self.assertEqual(""" +[Match] +MACAddress=aa:aa:aa:aa:aa:aa +Type=ether + +[Network] +LinkLocalAddressing=no +LLDP=no +EmitLLDP=no +IPv6AcceptRA=no +IPv6SendRA=no +VLAN=vlan1 +VLAN=vlanInternet +""".strip(), res.strip()) + + def test_template_vlan(self): + self.maxDiff = None + + vars = defaults.copy() + + vars.update(item=dict( + name="vlanProvider", + id=4001, + address="1.1.1.0/29", + ),) + + templar = Templar(loader=None, variables=vars) + res = templar.template(read_template_file('vlan.netdev.j2')) + + self.assertEqual(""" +[NetDev] +Name=vlanProvider +Kind=vlan + +[VLAN] +Id=4001 +""".strip(), res.strip()) + + res = templar.template(read_template_file('vlan.network.j2')) + + self.assertEqual(""" +[Match] +Name=vlanProvider +Type=vlan + +[Link] +MTUBytes=9000 + +[Network] +Address=1.1.1.0/29 +""".strip(), res.strip())