From 4eb3dd55defd801a5032efa597983cbbec4bbeea Mon Sep 17 00:00:00 2001 From: Sergei Petrosian Date: Wed, 30 Mar 2022 20:50:38 +0200 Subject: [PATCH] Add mssql_ha_configure functionality Add firewall, ha_cluster roles, required vars Take setup tasks form ha_cluster role, the mssql_ha_replica_type fact Print sqlcmd output only when it's available Add HA functionality description to README.md Add dependency for fedora.linux-system-roles in galaxy.yml Not print output of gathering facts because output is too long with -v Wait for mssql-server to prepare for client connections after restart Only print sqlcmd output when it exists Add tests for templates, fix bugs in templates Add error message when running against RHEL < 8 Add mssql_ha_cluster_run_role and other related variables Add mssql_ha_cluster_run_role Add mssql_ha_stonith_resources Add mssql_ha_cluster_print_vars --- .collection/galaxy.yml | 3 +- README.md | 552 +++++++++++++- defaults/main.yml | 14 + handlers/main.yml | 2 + meta/collection-requirements.yml | 2 + tasks/main.yml | 318 ++++++++ templates/configure_ag.j2 | 235 ++++++ templates/configure_endpoint.j2 | 108 +++ templates/create_and_back_up_cert.j2 | 73 ++ templates/create_ha_login.j2 | 26 + templates/create_master_key_encryption.j2 | 37 + templates/drop_cert.j2 | 30 + templates/enable_alwayson.j2 | 15 + templates/grant_permissions_to_ha_login.j2 | 7 + templates/join_to_ag.j2 | 58 ++ templates/replicate_db.j2 | 69 ++ templates/restore_cert.j2 | 19 + templates/verify_sql_cluster.j2 | 31 + tests/clean_up_mssql_pacemaker.yml | 27 + tests/templates/alter_ag.j2 | 103 +++ tests/templates/alter_endpoint.j2 | 24 + .../configure_ag_cluster_type_none.j2 | 10 + tests/templates/create_example_db.j2 | 38 + tests/templates/disable_alwayson.j2 | 1 + tests/templates/drop_ag.j2 | 13 + tests/templates/drop_endpoint.j2 | 14 + tests/templates/remove_sql_cert.j2 | 1 + tests/templates/separate_from_ag.j2 | 2 + tests/tests_configure_ha_cluster.yml | 715 ++++++++++++++++++ vars/main.yml | 34 + 30 files changed, 2578 insertions(+), 3 deletions(-) create mode 100644 meta/collection-requirements.yml create mode 100644 templates/configure_ag.j2 create mode 100644 templates/configure_endpoint.j2 create mode 100644 templates/create_and_back_up_cert.j2 create mode 100644 templates/create_ha_login.j2 create mode 100644 templates/create_master_key_encryption.j2 create mode 100644 templates/drop_cert.j2 create mode 100644 templates/enable_alwayson.j2 create mode 100644 templates/grant_permissions_to_ha_login.j2 create mode 100644 templates/join_to_ag.j2 create mode 100644 templates/replicate_db.j2 create mode 100644 templates/restore_cert.j2 create mode 100644 templates/verify_sql_cluster.j2 create mode 100644 tests/clean_up_mssql_pacemaker.yml create mode 100644 tests/templates/alter_ag.j2 create mode 100644 tests/templates/alter_endpoint.j2 create mode 100644 tests/templates/configure_ag_cluster_type_none.j2 create mode 100644 tests/templates/create_example_db.j2 create mode 100644 tests/templates/disable_alwayson.j2 create mode 100644 tests/templates/drop_ag.j2 create mode 100644 tests/templates/drop_endpoint.j2 create mode 100644 tests/templates/remove_sql_cert.j2 create mode 100644 tests/templates/separate_from_ag.j2 create mode 100644 tests/tests_configure_ha_cluster.yml diff --git a/.collection/galaxy.yml b/.collection/galaxy.yml index 87821487..8c04c790 100644 --- a/.collection/galaxy.yml +++ b/.collection/galaxy.yml @@ -16,7 +16,8 @@ readme: "README.md" license: - MIT -dependencies: {} +dependencies: + - "fedora.linux-system-roles": "*" tags: - "mssql" diff --git a/README.md b/README.md index cbfdbc4c..87e9b5a1 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ Type: `bool` ### `mssql_tls_enable` -Use the variables starting with `mssql_tls` to configure SQL Server to encrypt connections using TLS certificates. +Use the variables starting with the `mssql_tls_` prefix to configure SQL Server to encrypt connections using TLS certificates. You are responsible for creating and securing TLS certificate and private key files. It is assumed you have a CA that can issue these files. @@ -294,7 +294,7 @@ Type: `string` ### `mssql_server_repository` -The URL to the Microsoft SQL Server repository. +The URL to the Microsoft SQL Server repository. See `vars/` for default values based on operating system. Default: `{{ __mssql_server_repository }}` @@ -310,6 +310,173 @@ Default: `{{ __mssql_client_repository }}` Type: `string` +### `mssql_ha_configure` + +Use the variables starting with the `mssql_ha_` prefix to configure an SQL Server Always On availability group to provide high availability. + +Configuring for high availability is not supported on RHEL 7 because the System Roles ha_cluster role does not support RHEL 7. + +Set to `true` to configure for high availability. +Setting to `false` does not remove configuration for high availability. + +When set to `true`, the role performs the following tasks: + +1. Include the System Roles firewall role to configure firewall: +1.1. Open the firewall port set with the `mssql_ha_listener_port` variable. +1.2. Enable the `high-availability` service in firewall. +2. Configure SQL Server for high availability: +2.1. Enable AlwaysOn Health events. +2.2. Create certificate on the primary replica and distribute to other replicas. +2.3. Configure endpoint and availability group. +2.4. Configure the user provided with the `mssql_ha_login` variable for +Pacemaker. +1. Optional: Include the System Roles `ha_cluster` role to configure Pacemaker. +You must set [`mssql_ha_cluster_run_role`](#mssql_ha_cluster_run_role) to `true` and provide all variables required by the `ha_cluster` role for a proper Pacemaker cluster configuration based on example playbooks in [Setting Up SQL Server and Configuring for High Availability](#Setting_Up_SQL_Server_and_Configuring_for_High_Availability). + +Default: `false` + +Type: `bool` + +#### `mssql_ha_replica_type` + +A host variable that specifies the type of the replica to be configured on this host. + +See [`Setting Up SQL Server and Configuring for High Availability`](#Setting-Up-SQL-Server-and-Configuring-for-High-Availability) for an example inventory. + +You must set the `mssql_ha_replica_type` variable to `primary` for exactly one host. + +The available values are: `primary`, `synchronous`, `witness`. + +Default: no default + +Type: `str` + +#### `mssql_ha_firewall_configure` + +Whether to open ports in the Linux firewall for an Always On availability group. + +The role uses the System Roles firewall role to manage the firewall, hence, only firewall implementations supported by the firewall role work. + +If you set this variable to `false`, you must open the port defined with the `mssql_ha_listener_port` variable prior to running this role. + +Default: `true` + +Type: `bool` + +#### `mssql_ha_listener_port` + +The TCP port used to replicate data for an Always On availability group. + +Default: `5022` + +Type: `int` + +#### `mssql_ha_cert_name` + +The name of the certificate used to secure transactions between members of an Always On availability group. + +Default: `null` + +Type: `str` + +#### `mssql_ha_master_key_password` + +The password to set for the master key used with the certificate. + +Default: `null` + +Type: `str` + +#### `mssql_ha_private_key_password` + +The password to set for the private key used with the certificate. + +Default: `null` + +Type: `str` + +#### `mssql_ha_reset_cert` + +Whether to reset certificates used by an Always On availability group or not. + +Default: `false` + +Type: `bool` + +#### `mssql_ha_endpoint_name` + +The name of the endpoint to be configured. + +Default: `null` + +Type: `string` + +#### `mssql_ha_ag_name` + +The name of the availability group to be configured. + +Default: `null` + +Type: `string` + +#### `mssql_ha_db_name` + +The name of the database to be replicated. +This database must exist in SQL Server. + +Default: `null` + +Type: `string` + +#### `mssql_ha_db_backup_path` + +For SQL Server, any database participating in an Availability Group must be in a full recovery mode and have a valid log backup. +The role uses this path to backup the database provided with `mssql_ha_db_name` prior to initiating replication within an Always On availability group. + +The role backs up the database provided with `mssql_ha_db_backup_path` if no back up newer than 3 hours exists. + +Default: `/var/opt/mssql/data/{{ mssql_ha_db_name }}.bak` + +Type: `string` + +#### `mssql_ha_login` + +The user created for Pacemaker in SQL Server. +This user is used by the SQL Server Pacemaker resource agent to connect to SQL Server to perform regular database health checks and manage state transitions from replica to primary when needed. + +Default: `null` + +Type: `string` + +#### `mssql_ha_login_password` + +The password for the mssql_ha_login user in SQL Server. + +Default: `null` + +Type: `string` + +#### `mssql_ha_cluster_run_role` + +Whether to run the `ha_cluster` role from this role. + +Note that the `ha_cluster` role has the following limitation: + +**The role replaces the configuration of HA Cluster on specified nodes. +Any settings not specified in the role variables will be lost.** + +It means that the `microsoft.sql.server` role cannot run the `ha_cluster` role to avoid overwriting any existing Pacemaker configuration. + +To work around this limitation, the `microsoft.sql.server` role does not set any variables for the `ha_cluster` role to ensure that any existing Pacemaker configuration is not re-written. + +If you want the `microsoft.sql.server` to run the `ha_cluster` role, set `mssql_ha_cluster_run_role: true` and provide variables for the `ha_cluster` role with the `microsoft.sql.server` role invocation based on example playbooks in [Setting Up SQL Server and Configuring for High Availability](#Setting_Up_SQL_Server_and_Configuring_for_High_Availability). + +If you do not want the `microsoft.sql.server` to run the `ha_cluster` role and instead want to run the `ha_cluster` role independently of the `microsoft.sql.server` role, set `mssql_ha_cluster_run_role: false`. + +Default: `false` + +Type: `string` + ## Example Playbooks This section outlines example playbooks that you can use as a reference. @@ -396,6 +563,387 @@ This example shows how to use the role to set up SQL Server and configure it to - microsoft.sql.server ``` +### Setting Up SQL Server and Configuring for High Availability + +Examples in this section shows how to use the role to set up SQL Server and configure it for high availability in different environments. + +#### Configuring the Ansible Inventory + +You must set the `mssql_ha_replica_type` variable for each host that you want to configure. + +If you set [`mssql_ha_cluster_run_role`](#mssql_ha_cluster_run_role) to `true`, you can optionally provide variables required by the `ha_cluster` role. +If you do not provide names or addresses, the `ha_cluster` uses play's targets. +See the `ha_cluster` role's documentation for more information. + +Example inventory: + +```yaml +all: + hosts: + host1: + mssql_ha_replica_type: primary + host2: + mssql_ha_replica_type: synchronous + host3: + mssql_ha_replica_type: witness +``` + +#### Configuring SQL Server HA without Pacemaker + +If you want to configure Pacemaker independently, you can set [`mssql_ha_cluster_run_role`](#mssql_ha_cluster_run_role) to `false` to not include the `ha_cluster` role. + +Note that production environments require Pacemaker configured with fencing agents. + +```yaml +- hosts: all + vars: + mssql_accept_microsoft_odbc_driver_17_for_sql_server_eula: true + mssql_accept_microsoft_cli_utilities_for_sql_server_eula: true + mssql_accept_microsoft_sql_server_standard_eula: true + mssql_password: "p@55w0rD" + mssql_edition: Evaluation + mssql_ha_configure: true + mssql_ha_firewall_configure: true + mssql_ha_listener_port: 5022 + mssql_ha_cert_name: ExampleCert + mssql_ha_master_key_password: "p@55w0rD1" + mssql_ha_private_key_password: "p@55w0rD2" + mssql_ha_reset_cert: false + mssql_ha_endpoint_name: Example_Endpoint + mssql_ha_ag_name: ExampleAG + mssql_ha_db_name: ExampleDB + mssql_ha_login: ExamleLogin + mssql_ha_login_password: "p@55w0rD3" + mssql_ha_cluster_run_role: false + roles: + - microsoft.sql.server +``` + +#### Configuring SQL Server with HA and Pacemaker on Bare Metal + +If you want to configure Pacemaker from this role, you can set [`mssql_ha_cluster_run_role`](#mssql_ha_cluster_run_role) to `true` and provide variables required by the `ha_cluster` role to configure Pacemaker for your environment properly. +See the `ha_cluster` role's documentation for more information. + +Note that production environments require Pacemaker configured with fencing agents, this example playbook configures the `stonith:fence_apc_snmp` agent. + +```yaml +- hosts: all + vars: + mssql_accept_microsoft_odbc_driver_17_for_sql_server_eula: true + mssql_accept_microsoft_cli_utilities_for_sql_server_eula: true + mssql_accept_microsoft_sql_server_standard_eula: true + mssql_password: "p@55w0rD" + mssql_edition: Evaluation + mssql_ha_configure: true + mssql_ha_firewall_configure: true + mssql_ha_listener_port: 5022 + mssql_ha_cert_name: ExampleCert + mssql_ha_master_key_password: "p@55w0rD1" + mssql_ha_private_key_password: "p@55w0rD2" + mssql_ha_reset_cert: false + mssql_ha_endpoint_name: Example_Endpoint + mssql_ha_ag_name: ExampleAG + mssql_ha_db_name: ExampleDB + mssql_ha_login: ExamleLogin + mssql_ha_login_password: "p@55w0rD3" + mssql_ha_cluster_run_role: true + ha_cluster_cluster_name: "{{ mssql_ha_ag_name }}" + ha_cluster_hacluster_password: "p@55w0rD4" + ha_cluster_cluster_properties: + - attrs: + - name: cluster-recheck-interval + value: 2min + - name: start-failure-is-fatal + value: true + - name: stonith-enabled + value: true + ha_cluster_resource_primitives: + - id: Example_apc + agent: stonith:fence_apc_snmp + instance_attrs: + - attrs: + - name: login + value: apc_login + - name: passwd + value: apc_pass + - name: ipaddr + value: apc-switch.example.com + - name: pcmk_host_map + value: rhel8-node1.example.com:1;rhel8-node2.example.com:2 + - id: ag_cluster + agent: ocf:mssql:ag + instance_attrs: + - attrs: + - name: ag_name + value: "{{ mssql_ha_ag_name }}" + meta_attrs: + - attrs: + - name: failure-timeout + value: 60s + - id: virtualip + agent: ocf:heartbeat:IPaddr2 + instance_attrs: + - attrs: + - name: ip + value: 192.XXX.XXX.XXX + operations: + - action: monitor + attrs: + - name: interval + value: 30s + ha_cluster_resource_clones: + - resource_id: ag_cluster + promotable: yes + meta_attrs: + - attrs: + - name: notify + value: true + ha_cluster_constraints_colocation: + - resource_leader: + id: ag_cluster-clone + role: Promoted + resource_follower: + id: virtualip + options: + - name: score + value: INFINITY + ha_cluster_constraints_order: + - resource_first: + id: ag_cluster-clone + action: promote + resource_then: + id: virtualip + action: start + roles: + - microsoft.sql.server +``` + +#### Configuring SQL Server with HA and Pacemaker on VMWare + +If you want to configure Pacemaker from this role, you can set [`mssql_ha_cluster_run_role`](#mssql_ha_cluster_run_role) to `true` and provide variables required by the `ha_cluster` role to configure Pacemaker for your environment properly. +See the `ha_cluster` role's documentation for more information. + +Note that production environments require Pacemaker configured with fencing agents, this example playbook configures the `stonith:fence_vmware_soap` agent. + +```yaml +- hosts: all + vars: + mssql_accept_microsoft_odbc_driver_17_for_sql_server_eula: true + mssql_accept_microsoft_cli_utilities_for_sql_server_eula: true + mssql_accept_microsoft_sql_server_standard_eula: true + mssql_password: "p@55w0rD" + mssql_edition: Evaluation + mssql_ha_configure: true + mssql_ha_firewall_configure: true + mssql_ha_listener_port: 5022 + mssql_ha_cert_name: ExampleCert + mssql_ha_master_key_password: "p@55w0rD1" + mssql_ha_private_key_password: "p@55w0rD2" + mssql_ha_reset_cert: false + mssql_ha_endpoint_name: Example_Endpoint + mssql_ha_ag_name: ExampleAG + mssql_ha_db_name: ExampleDB + mssql_ha_login: ExamleLogin + mssql_ha_login_password: "p@55w0rD3" + mssql_ha_cluster_run_role: true + ha_cluster_cluster_name: "{{ mssql_ha_ag_name }}" + ha_cluster_hacluster_password: "p@55w0rD4" + ha_cluster_cluster_properties: + - attrs: + - name: cluster-recheck-interval + value: 2min + - name: start-failure-is-fatal + value: true + - name: stonith-enabled + value: true + ha_cluster_resource_primitives: + - id: vmfence + agent: stonith:fence_vmware_soap + instance_attrs: + - attrs: + - name: username + value: vmware_Login + - name: passwd + value: vmware_password + - name: ip + value: vmware_ip + - name: ssl_insecure + value: 1 + - id: ag_cluster + agent: ocf:mssql:ag + instance_attrs: + - attrs: + - name: ag_name + value: "{{ mssql_ha_ag_name }}" + meta_attrs: + - attrs: + - name: failure-timeout + value: 60s + - id: virtualip + agent: ocf:heartbeat:IPaddr2 + instance_attrs: + - attrs: + - name: ip + value: 192.XXX.XXX.XXX + operations: + - action: monitor + attrs: + - name: interval + value: 30s + ha_cluster_resource_clones: + - resource_id: ag_cluster + promotable: yes + meta_attrs: + - attrs: + - name: notify + value: true + ha_cluster_constraints_colocation: + - resource_leader: + id: ag_cluster-clone + role: Promoted + resource_follower: + id: virtualip + options: + - name: score + value: INFINITY + ha_cluster_constraints_order: + - resource_first: + id: ag_cluster-clone + action: promote + resource_then: + id: virtualip + action: start + roles: + - microsoft.sql.server +``` + +#### Configuring SQL Server with HA and Pacemaker on Azure + +If you want to configure Pacemaker from this role, you can set [`mssql_ha_cluster_run_role`](#mssql_ha_cluster_run_role) to `true` and provide variables required by the `ha_cluster` role to configure Pacemaker for your environment properly. +See the `ha_cluster` role's documentation for more information. + +Note that production environments require Pacemaker configured with fencing agents, this example playbook configures the `stonith:fence_azure_arm` agent. + +```yaml +- hosts: all + vars: + mssql_accept_microsoft_odbc_driver_17_for_sql_server_eula: true + mssql_accept_microsoft_cli_utilities_for_sql_server_eula: true + mssql_accept_microsoft_sql_server_standard_eula: true + mssql_password: "p@55w0rD" + mssql_edition: Evaluation + mssql_ha_configure: true + mssql_ha_firewall_configure: true + mssql_ha_listener_port: 5022 + mssql_ha_cert_name: ExampleCert + mssql_ha_master_key_password: "p@55w0rD1" + mssql_ha_private_key_password: "p@55w0rD2" + mssql_ha_reset_cert: false + mssql_ha_endpoint_name: Example_Endpoint + mssql_ha_ag_name: ExampleAG + mssql_ha_db_name: ExampleDB + mssql_ha_login: ExamleLogin + mssql_ha_login_password: "p@55w0rD3" + mssql_ha_cluster_run_role: true + ha_cluster_cluster_name: "{{ mssql_ha_ag_name }}" + ha_cluster_hacluster_password: "p@55w0rD4" + ha_cluster_cluster_properties: + - attrs: + - name: cluster-recheck-interval + value: 2min + - name: start-failure-is-fatal + value: true + - name: stonith-enabled + value: true + - name: stonith-timeout + value: 900 + ha_cluster_resource_primitives: + - id: rsc_st_azure + agent: stonith:fence_azure_arm + instance_attrs: + - attrs: + - name: login + value: azure_login + - name: passwd + value: azure_password + - name: resourceGroup + value: azure_resourceGroup_name + - name: tenantId + value: azure_tenant_ID + - name: subscriptionId + value: azure_subscription_ID + - name: power_timeout + value: 240 + - name: pcmk_reboot_timeout + value: 900 + - id: azure_load_balancer + agent: azure-lb + instance_attrs: + - attrs: + - name: port + value: 1234 + - id: ag_cluster + agent: ocf:mssql:ag + instance_attrs: + - attrs: + - name: ag_name + value: "{{ mssql_ha_ag_name }}" + meta_attrs: + - attrs: + - name: failure-timeout + value: 60s + - id: virtualip + agent: ocf:heartbeat:IPaddr2 + instance_attrs: + - attrs: + - name: ip + value: 192.XXX.XXX.XXX + operations: + - action: monitor + attrs: + - name: interval + value: 30s + ha_cluster_resource_groups: + - id: virtualip_group + resource_ids: + - azure_load_balancer + - virtualip + ha_cluster_resource_clones: + - resource_id: ag_cluster + promotable: yes + meta_attrs: + - attrs: + - name: notify + value: true + ha_cluster_constraints_colocation: + - resource_leader: + id: ag_cluster-clone + role: Promoted + resource_follower: + id: azure_load_balancer + options: + - name: score + value: INFINITY + ha_cluster_constraints_order: + - resource_first: + id: ag_cluster-master + action: promote + resource_then: + id: azure_load_balancer + action: start + roles: + - microsoft.sql.server +``` + +After running the following example playbook, you must also add a listener pointing to Azure using the following SQL statement: + +```sql +ALTER AVAILABILITY GROUP ExampleAG ADD LISTENER 'ExampleAG-listener' ( + WITH IP ( (azure_lb_ip),('255.255.255.0') ), + PORT = 1433 +); +``` + ## License MIT diff --git a/defaults/main.yml b/defaults/main.yml index 41b744fb..b8105883 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -25,3 +25,17 @@ mssql_tls_remote_src: false mssql_rpm_key: https://packages.microsoft.com/keys/microsoft.asc mssql_server_repository: "{{ __mssql_server_repository }}" mssql_client_repository: "{{ __mssql_client_repository }}" +mssql_ha_configure: false +mssql_ha_firewall_configure: true +mssql_ha_listener_port: 5022 +mssql_ha_cert_name: null +mssql_ha_private_key_password: null +mssql_ha_master_key_password: null +mssql_ha_reset_cert: false +mssql_ha_endpoint_name: null +mssql_ha_ag_name: null +mssql_ha_db_name: null +mssql_ha_db_backup_path: /var/opt/mssql/data/{{ mssql_ha_db_name }}.bak +mssql_ha_login: null +mssql_ha_login_password: null +mssql_ha_cluster_run_role: false diff --git a/handlers/main.yml b/handlers/main.yml index 9ba51e60..6ff2f976 100644 --- a/handlers/main.yml +++ b/handlers/main.yml @@ -4,3 +4,5 @@ service: name: mssql-server state: restarted + when: not (__mssql_primary_restarted is changed or + __mssql_replica_restarted is changed) diff --git a/meta/collection-requirements.yml b/meta/collection-requirements.yml new file mode 100644 index 00000000..6a656f88 --- /dev/null +++ b/meta/collection-requirements.yml @@ -0,0 +1,2 @@ +collections: + - name: fedora.linux_system_roles diff --git a/tasks/main.yml b/tasks/main.yml index 6f4d3656..5c9ea3df 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -9,6 +9,10 @@ set_fact: __mssql_sqlcmd_login_cmd: null +- name: Unset the __mssql_sqlcmd_login_cmd fact + set_fact: + __mssql_sqlcmd_login_cmd: null + - name: Link the deprecated accept_microsoft_sql_server_2019_standard_eula fact set_fact: mssql_accept_microsoft_sql_server_standard_eula: >- @@ -43,6 +47,36 @@ - mssql_upgrade | bool - mssql_version | int == 2017 +- name: Verify that EL < 8 is not used with mssql_ha_configure=true + fail: + msg: mssql_ha_configure=true does not support running against EL 7 hosts + when: + - mssql_ha_configure | bool + - ansible_distribution in ['CentOS', 'RedHat'] + - ansible_distribution_version is version('8', '<') + +- name: Verify if the mssql_ha_replica_type variable is provided correctly + assert: + that: mssql_ha_replica_type in + ['primary', 'synchronous', 'witness', 'absent'] + fail_msg: >- + You must set the mssql_ha_replica_type variable to one of 'primary', + 'synchronous', 'witness', 'absent' + when: mssql_ha_configure | bool + +- name: Verify that 'mssql_ha_replica_type = primary' is provided once + assert: + that: ansible_play_hosts_all | + map('extract', hostvars, 'mssql_ha_replica_type') | + select('match', '^primary$') | + list | + length == 1 + fail_msg: >- + You must set the mssql_ha_replica_type variable to 'primary' for one of + your managed nodes + run_once: true + when: mssql_ha_configure | bool + - name: Gather package facts package_facts: manager: auto @@ -423,6 +457,290 @@ __mssql_conf_setting: "network forceencryption" __mssql_conf_setting_value: "{{ '1' if mssql_tls_enable else 'unset' }}" +- name: Open required firewall ports and set required facts + when: + - mssql_ha_configure | bool + block: + - name: >- + Open the {{ mssql_ha_listener_port }}/tcp port and + enable the high-availability service in firewall + when: mssql_ha_firewall_configure | bool + include_role: + name: fedora.linux_system_roles.firewall + vars: + firewall: + - port: "{{ mssql_ha_listener_port }}/tcp" + zone: public + state: enabled + permanent: true + runtime: true + - service: high-availability + state: enabled + permanent: true + runtime: true + + # This is required because by default variables in vars/main.yml are + # mapped into global variables and the role needs them in host variables + - name: Set host variables + set_fact: + __mssql_ha_availability_mode: "{{ __mssql_ha_availability_mode }}" + __mssql_ha_failover_mode: "{{ __mssql_ha_failover_mode }}" + __mssql_ha_seeding_mode: "{{ __mssql_ha_seeding_mode }}" + +- name: Configure availability group on the primary node + when: + - mssql_ha_configure | bool + - mssql_ha_replica_type == 'primary' + block: + - name: Ensure the {{ __mssql_server_ha_packages }} package + package: + name: "{{ __mssql_server_ha_packages }}" + state: "{{ 'present' if mssql_ha_configure | bool else 'absent' }}" + register: __mssql_server_ha_packages_install + + - name: Enable the hadrenabled setting + include_tasks: mssql_conf_setting.yml + vars: + __mssql_conf_setting: "hadr hadrenabled" + __mssql_conf_setting_value: 1 + + # meta: flush_handlers does not support when conditional + - name: Restart the mssql-server service if hadrenabled task was changed + service: + name: mssql-server + state: restarted + when: (__mssql_conf_set is changed) or + (__mssql_server_ha_packages_install is changed) + register: __mssql_primary_restarted + + - name: Enable AlwaysOn Health events + vars: + __mssql_input_sql_file: enable_alwayson.j2 + include_tasks: input_sql_file.yml + + - name: Remove certificate from SQL Server + vars: + __mssql_input_sql_file: drop_cert.j2 + include_tasks: input_sql_file.yml + when: mssql_ha_reset_cert | bool + + - name: Remove certificate and private key files + file: + path: "{{ item }}" + state: absent + loop: + - "{{ __mssql_ha_cert_dest }}" + - "{{ __mssql_ha_private_key_dest }}" + when: mssql_ha_reset_cert | bool + + - name: Create master key encryption + vars: + __mssql_input_sql_file: create_master_key_encryption.j2 + include_tasks: input_sql_file.yml + + - name: Create and back up certificate + vars: + __mssql_input_sql_file: create_and_back_up_cert.j2 + include_tasks: input_sql_file.yml + + # changed_when: false because the role removes cert files after using them + - name: >- + Fetch certificate and private key from the primary node to the control + node + fetch: + src: "{{ item.value }}" + dest: "{{ item.key }}" + flat: true + with_dict: + cert: "{{ __mssql_ha_cert_dest }}" + key: "{{ __mssql_ha_private_key_dest }}" + changed_when: false + + - name: Create database mirroring endpoints + vars: + __mssql_input_sql_file: configure_endpoint.j2 + include_tasks: input_sql_file.yml + + - name: Create the {{ mssql_ha_login }} + vars: + __mssql_input_sql_file: create_ha_login.j2 + include_tasks: input_sql_file.yml + + # Required for configure_ag.j2 to set WRITE_LEASE_VALIDITY based on RHEL ver + - name: Get mssql-server version to see if WRITE_LEASE_VALIDITY is available + package_facts: + manager: auto + no_log: true + + - name: Create the {{ mssql_ha_ag_name }} availability group + vars: + __mssql_input_sql_file: configure_ag.j2 + include_tasks: input_sql_file.yml + + - name: Grant permissions to the {{ mssql_ha_login }} login + vars: + __mssql_input_sql_file: grant_permissions_to_ha_login.j2 + include_tasks: input_sql_file.yml + + - name: Back up and replicate the {{ mssql_ha_db_name }} database + vars: + __mssql_input_sql_file: replicate_db.j2 + include_tasks: input_sql_file.yml + + # This is required because `any_errors_fatal: true` does not work within + # blocks that have rescue or always sections + - name: Set a fact to indicate successful set up on the primary replica + delegate_to: localhost + set_fact: + __mssql_primary_successful: true + run_once: true + rescue: + # changed_when: false because this task removes unused remnant of cert files + - name: Remove certificate and private key from the control node + delegate_to: localhost + file: + path: "{{ item }}" + state: absent + loop: + - cert + - key + changed_when: false + + - name: Fail because this rescue block does not actually rescue failed tasks + fail: + msg: Configuration tasks failed + +- name: Configure availability group on replicas + when: + - mssql_ha_configure | bool + - mssql_ha_replica_type in ['synchronous', 'witness'] + any_errors_fatal: true + block: + - name: Fail if the primary node failed + delegate_to: localhost + fail: + msg: "Halting playbook execution due to error on the primary node" + when: not __mssql_primary_successful | d(false) + run_once: true + + - name: Ensure the {{ __mssql_server_ha_packages }} package + package: + name: "{{ __mssql_server_ha_packages }}" + state: "{{ 'present' if mssql_ha_configure | bool else 'absent' }}" + register: __mssql_server_ha_packages_install + + - name: Enable the hadrenabled setting + include_tasks: mssql_conf_setting.yml + vars: + __mssql_conf_setting: "hadr hadrenabled" + __mssql_conf_setting_value: 1 + + # flush_handlers task does not support when conditional + - name: Restart the mssql-server service if hadrenabled task was changed + service: + name: mssql-server + state: restarted + when: (__mssql_conf_set is changed) or + (__mssql_server_ha_packages_install is changed) + register: __mssql_replica_restarted + + - name: Enable AlwaysOn Health events + vars: + __mssql_input_sql_file: enable_alwayson.j2 + include_tasks: input_sql_file.yml + + - name: Create master key encryption + vars: + __mssql_input_sql_file: create_master_key_encryption.j2 + include_tasks: input_sql_file.yml + + - name: Distribute certificate and private key to managed nodes + copy: + src: "{{ item.key }}" + dest: "{{ item.value }}" + owner: mssql + group: mssql + mode: 0660 + force: true + with_dict: + cert: "{{ __mssql_ha_cert_dest }}" + key: "{{ __mssql_ha_private_key_dest }}" + + - name: Remove certificate from SQL Server + vars: + __mssql_input_sql_file: drop_cert.j2 + include_tasks: input_sql_file.yml + when: mssql_ha_reset_cert | bool + + - name: Restore certificate + vars: + __mssql_input_sql_file: restore_cert.j2 + include_tasks: input_sql_file.yml + + - name: Create database mirroring endpoints + vars: + __mssql_input_sql_file: configure_endpoint.j2 + include_tasks: input_sql_file.yml + + - name: Create the {{ mssql_ha_login }} login + vars: + __mssql_input_sql_file: create_ha_login.j2 + include_tasks: input_sql_file.yml + + - name: Join synchronous and witness servers to the availability group + vars: + __mssql_input_sql_file: join_to_ag.j2 + include_tasks: input_sql_file.yml + + - name: Grant permissions to the {{ mssql_ha_login }} login + vars: + __mssql_input_sql_file: grant_permissions_to_ha_login.j2 + include_tasks: input_sql_file.yml + + - name: Verify if the {{ mssql_ha_db_name }} database exists on secondaries + vars: + __mssql_input_sql_file: verify_sql_cluster.j2 + include_tasks: input_sql_file.yml + when: mssql_ha_replica_type not in ['primary', 'witness'] + always: + # changed_when: false because this task removes unused remnant of cert files + - name: Remove certificate and private key from the control node + delegate_to: localhost + file: + path: "{{ item }}" + state: absent + loop: + - cert + - key + changed_when: false + +- name: Configure pacemaker + when: mssql_ha_configure | bool + block: + - name: Save credentials for the {{ mssql_ha_login }} SQL Server login + copy: + content: |- + {{ mssql_ha_login }} + {{ mssql_ha_login_password }} + dest: /var/opt/mssql/secrets/passwd + owner: root + group: root + mode: 0400 + force: true + + - name: Run ha_cluster to configure pacemaker + include_role: + name: fedora.linux_system_roles.ha_cluster + when: mssql_ha_cluster_run_role | bool + +- name: Verify if the {{ mssql_ha_db_name }} database exists + vars: + __mssql_input_sql_file: verify_sql_cluster.j2 + include_tasks: input_sql_file.yml + when: + - mssql_ha_configure | bool + - mssql_ha_replica_type != 'witness' + - name: Ensure the ansible_managed header in /var/opt/mssql/mssql.conf vars: __lsr_ansible_managed: "{{ lookup('template', 'get_ansible_managed.j2') }}" diff --git a/templates/configure_ag.j2 b/templates/configure_ag.j2 new file mode 100644 index 00000000..8eaff1e2 --- /dev/null +++ b/templates/configure_ag.j2 @@ -0,0 +1,235 @@ +IF EXISTS ( + SELECT name, cluster_type_desc + FROM sys.availability_groups + WHERE name = '{{ mssql_ha_ag_name }}' AND + cluster_type_desc != 'external' +) +BEGIN + PRINT 'The existing {{ mssql_ha_ag_name }} availability group has \ +incorrect cluster type set, dropping the group to re-create it'; + DROP AVAILABILITY GROUP {{ mssql_ha_ag_name }}; + PRINT 'The {{ mssql_ha_ag_name }} availability group dropped successfully'; +END + +IF EXISTS ( + SELECT name, cluster_type_desc + FROM sys.availability_groups + WHERE name = '{{ mssql_ha_ag_name }}' +) +BEGIN + PRINT 'Verifying the existing availability group {{ mssql_ha_ag_name }}' + IF NOT EXISTS ( + SELECT name, db_failover + FROM sys.availability_groups + WHERE name = '{{ mssql_ha_ag_name }}' AND + db_failover = 1 + ) + BEGIN + ALTER AVAILABILITY GROUP {{ mssql_ha_ag_name }} SET (DB_FAILOVER = ON) + PRINT 'Set DB_FAILOVER to ON succesfully' + END + ELSE + BEGIN + PRINT 'DB_FAILOVER = ON is already set, skipping' + END + PRINT 'Verifying replicas' +{% for item in ansible_play_hosts %} +{% if hostvars[item]['mssql_ha_replica_type'] != 'absent' %} + IF EXISTS ( + SELECT replica_server_name, availability_mode_desc + FROM sys.availability_replicas replicas + JOIN sys.availability_groups groups + ON replicas.group_id = groups.group_id + WHERE groups.name = '{{ mssql_ha_ag_name }}' AND + replicas.replica_server_name = + '{{ hostvars[item]['ansible_hostname'] }}' AND + replicas.availability_mode_desc != + '{{ hostvars[item]['__mssql_ha_availability_mode'] }}' + ) + BEGIN + PRINT '{{ hostvars[item]['ansible_hostname'] }}: The availability mode \ +of this availability replica does not match the required availability mode, \ +removing this replica re-create it'; + ALTER AVAILABILITY GROUP {{ mssql_ha_ag_name }} REMOVE REPLICA ON + N'{{ hostvars[item]['ansible_hostname'] }}' + PRINT '{{ hostvars[item]['ansible_hostname'] }}: Removed successfully' + END + IF NOT EXISTS ( + SELECT replica_server_name + FROM sys.availability_replicas replicas + JOIN sys.availability_groups groups + ON replicas.group_id = groups.group_id + WHERE groups.name = '{{ mssql_ha_ag_name }}' AND + replicas.replica_server_name = + '{{ hostvars[item]['ansible_hostname'] }}' + ) + BEGIN + PRINT 'Adding the {{ hostvars[item]['ansible_hostname'] }} \ +{{ hostvars[item]['mssql_ha_replica_type'] }} replica'; +{% if hostvars[item]['mssql_ha_replica_type'] == 'synchronous' %} + ALTER AVAILABILITY GROUP {{ mssql_ha_ag_name }} ADD REPLICA ON + N'{{ hostvars[item]['ansible_hostname'] }}' WITH ( + ENDPOINT_URL = N'tcp://{{ + hostvars[item]['ansible_fqdn'] }}:{{ mssql_ha_listener_port }}', + AVAILABILITY_MODE = {{ hostvars[item]['__mssql_ha_availability_mode'] }}, + FAILOVER_MODE = {{ hostvars[item]['__mssql_ha_failover_mode'] }}, + SEEDING_MODE = {{ hostvars[item]['__mssql_ha_seeding_mode'] }} + ); +{% elif hostvars[item]['mssql_ha_replica_type'] == 'witness' %} + ALTER AVAILABILITY GROUP {{ mssql_ha_ag_name }} ADD REPLICA ON + N'{{ hostvars[item]['ansible_hostname'] }}' WITH ( + ENDPOINT_URL = N'tcp://{{ hostvars[item]['ansible_fqdn'] }}:{{ mssql_ha_listener_port }}', + AVAILABILITY_MODE = {{ hostvars[item]['__mssql_ha_availability_mode'] }} + ); +{% endif %} + PRINT 'The {{ hostvars[item]['ansible_hostname'] }} \ +{{ hostvars[item]['mssql_ha_replica_type'] }} replica added successfully'; + END + ELSE + BEGIN + PRINT 'Verifying the existing replica {{ hostvars[item]['ansible_hostname'] }}'; +{% if (hostvars[item]['mssql_ha_replica_type'] == 'primary') or + (hostvars[item]['mssql_ha_replica_type'] == 'synchronous') %} +{% set ag_replica_settings = ({ +"endpoint_url":{ + "sql_setting_name":"ENDPOINT_URL", + "sys_setting_name":"endpoint_url", + "setting_value":"N'tcp://" + hostvars[item]['ansible_fqdn'] + ":" + + mssql_ha_listener_port | string + "'" +}, +"failover_mode":{ + "sql_setting_name":"FAILOVER_MODE", + "sys_setting_name":"failover_mode_desc", + "setting_value":hostvars[item]['__mssql_ha_failover_mode'] +}, +"seeding_mode":{ + "sql_setting_name":"SEEDING_MODE", + "sys_setting_name":"seeding_mode_desc", + "setting_value":hostvars[item]['__mssql_ha_seeding_mode'] +}, +"allow_connections":{ + "sql_setting_name": "SECONDARY_ROLE (ALLOW_CONNECTIONS = ALL)", + "sys_setting_name": "secondary_role_allow_connections_desc", + "setting_value": "ALL" +} +}) %} +{% elif hostvars[item]['mssql_ha_replica_type'] == 'witness' %} +{% set ag_replica_settings = ({ +"endpoint_url":{ + "sql_setting_name":"ENDPOINT_URL", + "sys_setting_name":"endpoint_url", + "setting_value":"N'tcp://" + hostvars[item]['ansible_fqdn'] + ":" + + mssql_ha_listener_port | string + "'" +} +}) %} +{% endif %} +{% for key, value in ag_replica_settings.items() %} + IF NOT EXISTS ( + SELECT replica_server_name, {{ value.sys_setting_name }} + FROM sys.availability_replicas replicas + JOIN sys.availability_groups groups + ON replicas.group_id = groups.group_id + WHERE groups.name = '{{ mssql_ha_ag_name }}' AND + replicas.replica_server_name = + '{{ hostvars[item]['ansible_hostname'] }}' AND +{% if key == 'endpoint_url' %} + {{ value.sys_setting_name }} = {{ value.setting_value }} +{% else %} + {{ value.sys_setting_name }} = '{{ value.setting_value }}' +{% endif %} + ) + BEGIN + ALTER AVAILABILITY GROUP {{ mssql_ha_ag_name }} MODIFY REPLICA ON + N'{{ hostvars[item]['ansible_hostname'] }}' WITH ( +{% if key == 'allow_connections' %} + {{ value.sql_setting_name }} +{% else %} + {{ value.sql_setting_name }} = {{ value.setting_value }} +{% endif %} + ); + PRINT '{{ hostvars[item]['ansible_hostname'] }}: \ +The {{ value.sql_setting_name }} setting on this \ +{{ hostvars[item]['mssql_ha_replica_type'] }} replica configured successfully'; + END + ELSE + BEGIN + PRINT '{{ hostvars[item]['ansible_hostname'] }}: \ +The {{ value.sql_setting_name }} setting on this \ +{{ hostvars[item]['mssql_ha_replica_type'] }} replica is already set \ +correctly, skipping'; + END +{% endfor %} + END +{% elif hostvars[item]['mssql_ha_replica_type'] == 'absent' %} + IF NOT EXISTS ( + SELECT replica_server_name + FROM sys.availability_replicas replicas + JOIN sys.availability_groups groups + ON replicas.group_id = groups.group_id + WHERE groups.name = '{{ mssql_ha_ag_name }}' AND + replicas.replica_server_name = + '{{ hostvars[item]['ansible_hostname'] }}' + ) + BEGIN + PRINT '{{ hostvars[item]['ansible_hostname'] }}: this replica is already \ +removed, skipping'; + END + ELSE + BEGIN + PRINT '{{ hostvars[item]['ansible_hostname'] }}: Removing this replica'; + ALTER AVAILABILITY GROUP {{ mssql_ha_ag_name }} + REMOVE REPLICA ON N'{{ hostvars[item]['ansible_hostname'] }}'; + PRINT '{{ hostvars[item]['ansible_hostname'] }}: This replica is removed \ +successfully'; + END +{% endif %} +{% endfor %} +END + +IF NOT EXISTS ( + SELECT name, cluster_type_desc + FROM sys.availability_groups + WHERE name = '{{ mssql_ha_ag_name }}' +) +BEGIN + PRINT 'Creating the {{ mssql_ha_ag_name }} availability group'; + CREATE AVAILABILITY GROUP {{ mssql_ha_ag_name }} +{% if ansible_os_family == 'RedHat' and + ansible_distribution_version is version('8.3', '<') %} + WITH (DB_FAILOVER = ON, CLUSTER_TYPE = EXTERNAL) +{% else %} + WITH (DB_FAILOVER = ON, CLUSTER_TYPE = EXTERNAL) +{% endif %} + FOR REPLICA ON + N'{{ ansible_hostname }}' WITH ( + ENDPOINT_URL = N'tcp://{{ ansible_fqdn }}:{{ mssql_ha_listener_port }}', + AVAILABILITY_MODE = {{ __mssql_ha_availability_mode }}, + FAILOVER_MODE = {{ __mssql_ha_failover_mode }}, + SEEDING_MODE = {{ __mssql_ha_seeding_mode }}, + SECONDARY_ROLE (ALLOW_CONNECTIONS = ALL) +{% for item in ansible_play_hosts %} +{% if hostvars[item]['mssql_ha_replica_type'] == 'synchronous' %} + ), + N'{{ hostvars[item]['ansible_hostname'] }}' WITH ( + ENDPOINT_URL = N'tcp://{{ + hostvars[item]['ansible_fqdn'] }}:{{ mssql_ha_listener_port }}', + AVAILABILITY_MODE = {{ hostvars[item]['__mssql_ha_availability_mode'] }}, + FAILOVER_MODE = {{ hostvars[item]['__mssql_ha_failover_mode'] }}, + SEEDING_MODE = {{ hostvars[item]['__mssql_ha_seeding_mode'] }}, + SECONDARY_ROLE (ALLOW_CONNECTIONS = ALL) +{% elif hostvars[item]['mssql_ha_replica_type'] == 'witness' %} + ), + N'{{ hostvars[item]['ansible_hostname'] }}' WITH ( + ENDPOINT_URL = N'tcp://{{ + hostvars[item]['ansible_fqdn'] }}:{{ mssql_ha_listener_port }}', + AVAILABILITY_MODE = {{ hostvars[item]['__mssql_ha_availability_mode'] }} +{% endif %} +{% endfor %} + ); + PRINT 'The {{ mssql_ha_ag_name }} availability group created successfully'; +END + +-- It is not possible to grant permissions fully idempotently +ALTER AVAILABILITY GROUP {{ mssql_ha_ag_name }} GRANT CREATE ANY DATABASE; +PRINT 'Granted the CREATE ANY DATABASE permission to the \ +{{ mssql_ha_ag_name }} availability group'; diff --git a/templates/configure_endpoint.j2 b/templates/configure_endpoint.j2 new file mode 100644 index 00000000..a21a6efa --- /dev/null +++ b/templates/configure_endpoint.j2 @@ -0,0 +1,108 @@ +IF NOT EXISTS ( + SELECT name + FROM sys.database_mirroring_endpoints + WHERE name = '{{ mssql_ha_endpoint_name }}' +) +BEGIN + PRINT 'Endpoint {{ mssql_ha_endpoint_name }} does not exist, creating'; + CREATE ENDPOINT {{ mssql_ha_endpoint_name }} + STATE = STARTED + AS TCP (LISTENER_PORT = {{ mssql_ha_listener_port }}) + FOR DATABASE_MIRRORING ( + ROLE = {{ __mssql_ha_endpoint_role }}, + AUTHENTICATION = CERTIFICATE {{ mssql_ha_cert_name }}, + ENCRYPTION = REQUIRED ALGORITHM AES + ); + PRINT 'Endpoint {{ mssql_ha_endpoint_name }} created successfully' +END +ELSE +BEGIN + PRINT 'Verifying the existing endpoint {{ mssql_ha_endpoint_name }}'; + IF NOT EXISTS ( + SELECT name, port FROM sys.tcp_endpoints + WHERE name = '{{ mssql_ha_endpoint_name }}' AND + port = {{ mssql_ha_listener_port }} + ) + BEGIN + ALTER ENDPOINT {{ mssql_ha_endpoint_name }} + AS TCP (LISTENER_PORT = {{ mssql_ha_listener_port }}); + PRINT 'The LISTENER_PORT setting for the {{ mssql_ha_endpoint_name }} \ +endpoint updated to {{ mssql_ha_listener_port }} successfully'; + END + ELSE + BEGIN + PRINT 'The LISTENER_PORT setting for the {{ mssql_ha_endpoint_name }} \ +endpoint is already set to {{ mssql_ha_listener_port }}, skipping'; + END + IF NOT EXISTS ( + SELECT name, role_desc FROM sys.database_mirroring_endpoints + WHERE name = '{{ mssql_ha_endpoint_name }}' AND + role_desc = '{{ __mssql_ha_endpoint_role }}' + ) + BEGIN + ALTER ENDPOINT {{ mssql_ha_endpoint_name }} + FOR DATABASE_MIRRORING (ROLE = {{ __mssql_ha_endpoint_role }}); + PRINT 'The ROLE setting for the {{ mssql_ha_endpoint_name }} \ +endpoint updated to {{ __mssql_ha_endpoint_role }} successfully'; + END + ELSE + BEGIN + PRINT 'The ROLE setting for the {{ mssql_ha_endpoint_name }} \ +endpoint is already set to {{ __mssql_ha_endpoint_role }}, skipping'; + END + IF NOT EXISTS ( + SELECT endp.name as endpoint_name, + cert.name as cert_name, + cert.certificate_id cert_id + FROM sys.certificates cert + JOIN sys.database_mirroring_endpoints endp + ON cert.certificate_id = endp.certificate_id + WHERE endp.name = '{{ mssql_ha_endpoint_name }}' AND + cert.name = '{{ mssql_ha_cert_name }}' + ) + BEGIN + ALTER ENDPOINT {{ mssql_ha_endpoint_name }} + FOR DATABASE_MIRRORING ( + AUTHENTICATION = CERTIFICATE {{ mssql_ha_cert_name }} + ); + PRINT 'The certificate for the {{ mssql_ha_endpoint_name }} \ +endpoint updated to {{ mssql_ha_cert_name }} successfully'; + END + ELSE + BEGIN + PRINT 'The certificate for the {{ mssql_ha_endpoint_name }} \ +endpoint is already set to {{ mssql_ha_cert_name }}, skipping'; + END + IF NOT EXISTS ( + SELECT name, encryption_algorithm_desc + FROM sys.database_mirroring_endpoints + WHERE name = '{{ mssql_ha_endpoint_name }}' AND + encryption_algorithm_desc = 'AES' + ) + BEGIN + ALTER ENDPOINT {{ mssql_ha_endpoint_name }} + FOR DATABASE_MIRRORING (ENCRYPTION = REQUIRED ALGORITHM AES); + PRINT 'The ENCRYPTION setting for the {{ mssql_ha_endpoint_name }} \ +endpoint updated to AES successfully'; + END + ELSE + BEGIN + PRINT 'The ENCRYPTION setting for the {{ mssql_ha_endpoint_name }} \ +endpoint is already set to AES, skipping'; + END + IF NOT EXISTS ( + SELECT name, state + FROM sys.tcp_endpoints + WHERE name = '{{ mssql_ha_endpoint_name }}' AND + state = 0 + ) + BEGIN + PRINT 'Endpoint {{ mssql_ha_endpoint_name }} is not started, starting'; + ALTER ENDPOINT {{ mssql_ha_endpoint_name }} STATE = STARTED; + PRINT 'Endpoint {{ mssql_ha_endpoint_name }} started successfully'; + END + ELSE + BEGIN + PRINT 'Endpoint {{ mssql_ha_endpoint_name }} is already started, skipping'; + END +END diff --git a/templates/create_and_back_up_cert.j2 b/templates/create_and_back_up_cert.j2 new file mode 100644 index 00000000..c49e047a --- /dev/null +++ b/templates/create_and_back_up_cert.j2 @@ -0,0 +1,73 @@ +-- Enabling NOCOUNT to suppress (1 rows affected) messages from DECLARE +-- keywords on the output +SET NOCOUNT ON; + +DECLARE @cerExists INT; +EXEC master.dbo.xp_fileexist '{{ __mssql_ha_cert_dest }}', @cerExists OUTPUT; +DECLARE @pvkExists INT; +EXEC master.dbo.xp_fileexist '{{ __mssql_ha_private_key_dest }}', + @pvkExists OUTPUT; + +IF NOT EXISTS( + SELECT name + FROM sys.certificates + WHERE name = '{{ mssql_ha_cert_name }}' +) +BEGIN + PRINT 'Certificate {{ mssql_ha_cert_name }} does not exist, creating'; + IF (@cerExists = 1 AND @pvkExists = 1) OR (@cerExists != @pvkExists) + BEGIN + THROW 51000, 'Certificate {{ mssql_ha_cert_name }} does not exist in \ +SQL Server, however, {{ __mssql_ha_cert_dest }} \ +and/or {{ __mssql_ha_private_key_dest }} files do exist. \ +You must either remove the files, or run the role with \ +`mssql_ha_reset_cert: true` to regenerate certificates. Ensure to read \ +carefully what `mssql_ha_reset_cert: true` does in the README.md file of the \ +role beforehand', 1; + END + ELSE + BEGIN + CREATE CERTIFICATE {{ mssql_ha_cert_name }} + WITH SUBJECT = 'Managed by microsoft.sql.server'; + PRINT 'Certificate {{ mssql_ha_cert_name }} created successfully'; + END +END +ELSE +BEGIN + PRINT 'Certificate {{ mssql_ha_cert_name }} already exists, skipping'; +END + +IF @cerExists = 1 AND @pvkExists = 1 +BEGIN + PRINT '{{ __mssql_ha_cert_dest }} and \ +{{ __mssql_ha_private_key_dest }} already exist, skipping'; +END +ELSE IF @cerExists = 0 AND @pvkExists = 0 +BEGIN + PRINT 'Exporting a certificate and private key to \ +{{ __mssql_ha_cert_dest }} and \ +{{ __mssql_ha_private_key_dest }}'; + BACKUP CERTIFICATE {{ mssql_ha_cert_name }} + TO FILE = '{{ __mssql_ha_cert_dest }}' + WITH PRIVATE KEY ( + FILE = '{{ __mssql_ha_private_key_dest }}', + ENCRYPTION BY PASSWORD = '{{ mssql_ha_private_key_password }}' + ); + PRINT 'Certificate and private key files \ +{{ __mssql_ha_cert_dest }} and \ +{{ __mssql_ha_private_key_dest }} exported successfully'; +END +ELSE IF @cerExists = 1 AND @pvkExists = 0 +BEGIN + PRINT '{{ __mssql_ha_private_key_dest }} does not exist \ +while {{ __mssql_ha_cert_dest }} exists. You must \ +either remove the files, or run the role with `mssql_ha_reset_cert: true` to \ +regenerate certificates.'; +END +ELSE IF @cerExists = 0 AND @pvkExists = 1 +BEGIN + PRINT '{{ __mssql_ha_cert_dest }} does not exist \ +while {{ __mssql_ha_private_key_dest }} exists. You must \ +either remove the files, or run the role with `mssql_ha_reset_cert: true` to \ +regenerate certificates.'; +END diff --git a/templates/create_ha_login.j2 b/templates/create_ha_login.j2 new file mode 100644 index 00000000..c455af65 --- /dev/null +++ b/templates/create_ha_login.j2 @@ -0,0 +1,26 @@ +USE master; +IF NOT EXISTS ( + SELECT name FROM sys.server_principals + WHERE name = '{{ mssql_ha_login }}' +) +BEGIN + PRINT 'A {{ mssql_ha_login }} login does not exist, creating'; + CREATE LOGIN {{ mssql_ha_login }} + WITH PASSWORD = N'{{ mssql_ha_login_password }}'; + PRINT 'The {{ mssql_ha_login }} login created successfully'; +END +ELSE +BEGIN + PRINT 'A {{ mssql_ha_login }} login already exists, skipping' +END + +IF IS_SRVROLEMEMBER ('sysadmin','{{ mssql_ha_login }}') = 1 +BEGIN + PRINT '{{ mssql_ha_login }} is a member of sysadmin role, skipping'; +END +ELSE +BEGIN + PRINT 'Adding {{ mssql_ha_login }} to the sysadmin server role'; + ALTER SERVER ROLE sysadmin ADD MEMBER {{ mssql_ha_login }}; + PRINT '{{ mssql_ha_login }} added to the sysadmin server role successfully'; +END diff --git a/templates/create_master_key_encryption.j2 b/templates/create_master_key_encryption.j2 new file mode 100644 index 00000000..72fa68c7 --- /dev/null +++ b/templates/create_master_key_encryption.j2 @@ -0,0 +1,37 @@ +IF NOT EXISTS ( + SELECT name + FROM sys.symmetric_keys + WHERE name LIKE '%databaseMasterkey%' +) +BEGIN + PRINT 'Master key does not exist, creating'; + CREATE MASTER KEY ENCRYPTION BY PASSWORD = + '{{ mssql_ha_master_key_password }}'; + PRINT 'Master key created successfully'; +END +ELSE +BEGIN + PRINT 'Master key already exists, verifying the provided password against \ +the existing master key'; + BEGIN TRY + OPEN MASTER KEY DECRYPTION BY PASSWORD = + '{{ mssql_ha_master_key_password }}'; + PRINT 'The provided master key password is correct'; + END TRY + BEGIN CATCH +{% if not mssql_ha_reset_cert %} + PRINT 'You provided an incorrect master key password with the \ +mssql_ha_master_key_password variable'; + THROW; +{% elif mssql_ha_reset_cert %} + PRINT 'Master key password provided with the \ +mssql_ha_master_key_password variable does not match the existing password, \ +dropping master key to re-create it'; + DROP MASTER KEY; + PRINT 'Master key dropped successfully'; + CREATE MASTER KEY ENCRYPTION BY PASSWORD = + '{{ mssql_ha_master_key_password }}'; + PRINT 'Master key created successfully'; +{% endif %} + END CATCH +END diff --git a/templates/drop_cert.j2 b/templates/drop_cert.j2 new file mode 100644 index 00000000..3888e34a --- /dev/null +++ b/templates/drop_cert.j2 @@ -0,0 +1,30 @@ +IF EXISTS( + SELECT name + FROM sys.certificates + WHERE name = '{{ mssql_ha_cert_name }}' +) +BEGIN + PRINT 'Certificate {{ mssql_ha_cert_name }} already exists, checking if \ +there is an endopint associated with this certificate'; + DECLARE @EndpointName VARCHAR(30) = ( + SELECT endp.name as endpoint_name + FROM sys.certificates cert + JOIN sys.database_mirroring_endpoints endp + ON cert.certificate_id = endp.certificate_id + WHERE cert.name = '{{ mssql_ha_cert_name }}' + ); + IF @EndpointName IS NOT NULL + BEGIN + PRINT 'Removing the endpoint ' + CAST(@EndpointName AS VARCHAR) + +' to re-create it'; + EXEC ('DROP ENDPOINT ' +@EndpointName); + PRINT 'The {{ mssql_ha_endpoint_name }} endpoint removed successfully'; + END + ELSE + BEGIN + PRINT 'There is no endpoint associated with this certificate, skipping'; + END + PRINT 'Removing the {{ mssql_ha_cert_name }} certificate to re-create it'; + DROP CERTIFICATE {{ mssql_ha_cert_name }}; + PRINT 'The {{ mssql_ha_cert_name }} certificate removed successfully' +END diff --git a/templates/enable_alwayson.j2 b/templates/enable_alwayson.j2 new file mode 100644 index 00000000..04e4982d --- /dev/null +++ b/templates/enable_alwayson.j2 @@ -0,0 +1,15 @@ +IF NOT EXISTS ( + SELECT name, startup_state + FROM sys.server_event_sessions WHERE + name = 'AlwaysOn_health' and + startup_state = 1 +) +BEGIN + PRINT 'AlwaysOn Health events are not enabled, enabling'; + ALTER EVENT SESSION AlwaysOn_health ON SERVER WITH (STARTUP_STATE=ON); + PRINT 'AlwaysOn Health events enabled successfully'; +END +ELSE +BEGIN + PRINT 'AlwaysOn Health events already enabled, skipping'; +END diff --git a/templates/grant_permissions_to_ha_login.j2 b/templates/grant_permissions_to_ha_login.j2 new file mode 100644 index 00000000..d72c767b --- /dev/null +++ b/templates/grant_permissions_to_ha_login.j2 @@ -0,0 +1,7 @@ +-- Need to find how to add permissions idempotently +PRINT 'Granting the required permissions to {{ mssql_ha_login }}'; +GRANT ALTER, CONTROL, VIEW DEFINITION ON + AVAILABILITY GROUP::{{ mssql_ha_ag_name }} + TO {{ mssql_ha_login }}; +GRANT VIEW SERVER STATE TO {{ mssql_ha_login }}; +PRINT 'Required permissions granted to {{ mssql_ha_login }}'; diff --git a/templates/join_to_ag.j2 b/templates/join_to_ag.j2 new file mode 100644 index 00000000..ad94d6b8 --- /dev/null +++ b/templates/join_to_ag.j2 @@ -0,0 +1,58 @@ +IF EXISTS ( + SELECT name + FROM sys.availability_groups + WHERE name = '{{ mssql_ha_ag_name }}' +) +AND NOT EXISTS ( + SELECT ag.name, replica.replica_server_name, replica.availability_mode_desc + FROM sys.availability_groups ag + JOIN sys.availability_replicas replica + ON ag.group_id = replica.group_id + WHERE ag.name = '{{ mssql_ha_ag_name }}' AND + replica.replica_server_name = '{{ ansible_hostname }}' AND + replica.availability_mode_desc = '{{ __mssql_ha_availability_mode }}' +) +BEGIN + PRINT 'The existing availability group {{ mssql_ha_ag_name }} has \ +incorrect availability mode set for the {{ ansible_hostname }} replica, \ +removing this availability group to re-create it' + DROP AVAILABILITY GROUP {{ mssql_ha_ag_name }}; + PRINT 'The availability group {{ mssql_ha_ag_name }} removed successfully' +END +ELSE IF EXISTS ( + SELECT name + FROM sys.availability_groups + WHERE name = '{{ mssql_ha_ag_name }}' +) +AND EXISTS ( + SELECT ag.name, replica.replica_server_name, replica.availability_mode_desc + FROM sys.availability_groups ag + JOIN sys.availability_replicas replica + ON ag.group_id = replica.group_id + WHERE ag.name = '{{ mssql_ha_ag_name }}' AND + replica.replica_server_name = '{{ ansible_hostname }}' AND + replica.availability_mode_desc = '{{ __mssql_ha_availability_mode }}' +) +BEGIN + PRINT 'Already joined to the {{ mssql_ha_ag_name }} availability group, \ +skipping' +END + +IF NOT EXISTS( + SELECT name + FROM sys.availability_groups + WHERE name = '{{ mssql_ha_ag_name }}' +) +BEGIN + PRINT 'Joining to the {{ mssql_ha_ag_name }} availability group'; + ALTER AVAILABILITY GROUP {{ mssql_ha_ag_name }} + JOIN WITH (CLUSTER_TYPE = EXTERNAL); + PRINT 'Joined to the {{ mssql_ha_ag_name }} availability group successfully'; +END + +{% if mssql_ha_replica_type == 'synchronous' %} +-- It is not possible to grant permissions fully idempotently +ALTER AVAILABILITY GROUP {{ mssql_ha_ag_name }} GRANT CREATE ANY DATABASE; +PRINT 'Granted the CREATE ANY DATABASE permission to the \ +{{ mssql_ha_ag_name }} availability group'; +{% endif %} diff --git a/templates/replicate_db.j2 b/templates/replicate_db.j2 new file mode 100644 index 00000000..bf703bfa --- /dev/null +++ b/templates/replicate_db.j2 @@ -0,0 +1,69 @@ +IF NOT EXISTS ( + SELECT name, recovery_model_desc + FROM sys.databases + WHERE name = '{{ mssql_ha_db_name }}' AND + recovery_model_desc = 'FULL' +) +BEGIN + PRINT 'Setting RECOVERY FULL on the {{ mssql_ha_db_name }} database'; + ALTER DATABASE {{ mssql_ha_db_name }} SET RECOVERY FULL; + PRINT 'RECOVERY FULL on the {{ mssql_ha_db_name }} database set successfully'; +END +ELSE +BEGIN + PRINT 'RECOVERY FULL on the {{ mssql_ha_db_name }} database is set, skipping'; +END + +IF NOT EXISTS ( + SELECT [database_name], backup_start_date, backup_finish_date, [type] + FROM msdb.dbo.backupset + WHERE [type]='D' AND + [database_name]='{{ mssql_ha_db_name }}' AND + backup_finish_date >= DATEADD(hh, -3, GETDATE()) +) +BEGIN + PRINT 'Backing up the {{ mssql_ha_db_name }} database to \ +{{ mssql_ha_db_backup_path }}'; + BACKUP DATABASE {{ mssql_ha_db_name }} + TO DISK = N'{{ mssql_ha_db_backup_path }}'; + PRINT 'The {{ mssql_ha_db_name }} database backed up successfully'; +END +ELSE +BEGIN + PRINT 'The {{ mssql_ha_db_name }} database is already backed up, skipping'; +END + +IF NOT EXISTS ( + SELECT + AG.name AS [AvailabilityGroupName], + ISNULL(agstates.primary_replica, '') AS [PrimaryReplicaServerName], + dbcs.database_name AS [DatabaseName], + ISNULL(dbrs.synchronization_state, 0) AS [SynchronizationState], + ISNULL(dbrs.is_suspended, 0) AS [IsSuspended], + ISNULL(dbcs.is_database_joined, 0) AS [IsJoined] + FROM master.sys.availability_groups AS AG + LEFT OUTER JOIN master.sys.dm_hadr_availability_group_states as agstates + ON AG.group_id = agstates.group_id + INNER JOIN master.sys.availability_replicas AS AR + ON AG.group_id = AR.group_id + INNER JOIN master.sys.dm_hadr_availability_replica_states AS arstates + ON AR.replica_id = arstates.replica_id AND arstates.is_local = 1 + INNER JOIN master.sys.dm_hadr_database_replica_cluster_states AS dbcs + ON arstates.replica_id = dbcs.replica_id + LEFT OUTER JOIN master.sys.dm_hadr_database_replica_states AS dbrs + ON dbcs.replica_id = dbrs.replica_id + AND dbcs.group_database_id = dbrs.group_database_id +) +BEGIN + PRINT 'Adding the {{ mssql_ha_db_name }} database to the \ +{{ mssql_ha_ag_name }} availability group'; + ALTER AVAILABILITY GROUP {{ mssql_ha_ag_name }} + ADD DATABASE {{ mssql_ha_db_name }}; + PRINT 'The {{ mssql_ha_db_name }} database added to the \ +{{ mssql_ha_ag_name }} availability group successfully'; +END +ELSE +BEGIN + PRINT 'The {{ mssql_ha_db_name }} database is already added to the \ +{{ mssql_ha_ag_name }} availability group, skipping'; +END diff --git a/templates/restore_cert.j2 b/templates/restore_cert.j2 new file mode 100644 index 00000000..05b4de23 --- /dev/null +++ b/templates/restore_cert.j2 @@ -0,0 +1,19 @@ +IF NOT EXISTS( + SELECT name + FROM sys.certificates + WHERE name = '{{ mssql_ha_cert_name }}' +) +BEGIN + PRINT 'Certificate {{ mssql_ha_cert_name }} does not exist, creating'; + CREATE CERTIFICATE {{ mssql_ha_cert_name }} + FROM FILE = '{{ __mssql_ha_cert_dest }}' + WITH PRIVATE KEY ( + FILE = '{{ __mssql_ha_private_key_dest }}', + DECRYPTION BY PASSWORD = '{{ mssql_ha_private_key_password }}' + ); + PRINT 'Certificate {{ mssql_ha_cert_name }} created successfully'; +END +ELSE +BEGIN + PRINT 'Certificate {{ mssql_ha_cert_name }} already exists, skipping'; +END diff --git a/templates/verify_sql_cluster.j2 b/templates/verify_sql_cluster.j2 new file mode 100644 index 00000000..0e8b5e23 --- /dev/null +++ b/templates/verify_sql_cluster.j2 @@ -0,0 +1,31 @@ +PRINT 'Checking if the {{ mssql_ha_db_name }} database exists on this replica'; +IF NOT EXISTS( + SELECT name + FROM sys.databases + WHERE name = '{{ mssql_ha_db_name }}' +) +BEGIN + THROW 51000, 'The databse {{ mssql_ha_db_name }} does not exist in \ +sys.databases', 1; +END +ELSE +BEGIN + PRINT 'Verified that the {{ mssql_ha_db_name }} database exists in \ +sys.databases'; +END + + +IF NOT EXISTS( + SELECT DB_NAME(database_id) AS 'database', + synchronization_state_desc + FROM sys.dm_hadr_database_replica_states +) +BEGIN + THROW 51000, 'The databse {{ mssql_ha_db_name }} does not exist in \ +sys.dm_hadr_database_replica_states', 1; +END +ELSE +BEGIN + PRINT 'Verified that the {{ mssql_ha_db_name }} database exists in \ +sys.dm_hadr_database_replica_states'; +END diff --git a/tests/clean_up_mssql_pacemaker.yml b/tests/clean_up_mssql_pacemaker.yml new file mode 100644 index 00000000..28d2f2dc --- /dev/null +++ b/tests/clean_up_mssql_pacemaker.yml @@ -0,0 +1,27 @@ +--- +- name: Purge cluster and mssql configuration + hosts: all + tasks: + - name: Purge cluster configuration + vars: + ha_cluster_cluster_present: no + ha_cluster_enable_repos: false + include_role: + name: fedora.linux_system_roles.ha_cluster + + - name: Remove cluster RPMs + package: + name: + - pacemaker + - pcs + - fence-agents-all + - resource-agents + state: absent + + - name: Remove mssql RPMs + shell: dnf remove mssql-* -y + changed_when: true + + - name: Remove related files + shell: rm -rf /var/opt/mssql/ /opt/mssql* /tmp/ansible.* + changed_when: true diff --git a/tests/templates/alter_ag.j2 b/tests/templates/alter_ag.j2 new file mode 100644 index 00000000..a618944a --- /dev/null +++ b/tests/templates/alter_ag.j2 @@ -0,0 +1,103 @@ +IF EXISTS ( + SELECT name, cluster_type_desc + FROM sys.availability_groups + WHERE name = '{{ mssql_ha_ag_name }}' +) +BEGIN + PRINT 'Altering the existing availability group {{ mssql_ha_ag_name }}' + IF NOT EXISTS ( + SELECT name, db_failover + FROM sys.availability_groups + WHERE name = '{{ mssql_ha_ag_name }}' AND + db_failover = 0 + ) + BEGIN + ALTER AVAILABILITY GROUP {{ mssql_ha_ag_name }} SET (DB_FAILOVER = ON) + PRINT 'Set DB_FAILOVER to ON succesfully' + END + ELSE + BEGIN + PRINT 'DB_FAILOVER = OFF is already set, skipping' + END + PRINT 'Altering replicas' +{% for item in ansible_play_hosts %} +{% if hostvars[item]['mssql_ha_replica_type'] != 'absent' %} + IF EXISTS ( + SELECT replica_server_name + FROM sys.availability_replicas + WHERE replica_server_name = '{{ hostvars[item]['ansible_hostname'] }}' + ) + BEGIN + PRINT 'Verifying the existing replica {{ item }}'; +{% if (hostvars[item]['mssql_ha_replica_type'] == 'primary') or + (hostvars[item]['mssql_ha_replica_type'] == 'synchronous') %} +{% set ag_replica_settings = ({ +"endpoint_url":{ + "sql_setting_name":"ENDPOINT_URL", + "sys_setting_name":"endpoint_url", + "setting_value":"N'tcp://" + hostvars[item]['ansible_fqdn'] + ":" + + "1234" + "'" +}, +"failover_mode":{ + "sql_setting_name":"FAILOVER_MODE", + "sys_setting_name":"failover_mode_desc", + "setting_value":"EXTERNAL" +}, +"seeding_mode":{ + "sql_setting_name":"SEEDING_MODE", + "sys_setting_name":"seeding_mode_desc", + "setting_value":"MANUAL" +}, +"allow_connections":{ + "sql_setting_name": "SECONDARY_ROLE (ALLOW_CONNECTIONS = NO)", + "sys_setting_name": "secondary_role_allow_connections_desc", + "setting_value": "NO" +} +}) %} +{% elif hostvars[item]['mssql_ha_replica_type'] == 'witness' %} +{% set ag_replica_settings = ({ +"endpoint_url":{ + "sql_setting_name":"ENDPOINT_URL", + "sys_setting_name":"endpoint_url", + "setting_value":"N'tcp://" + hostvars[item]['ansible_fqdn'] + ":" + + "1234" + "'" +} +}) %} +{% endif %} +{% for key, value in ag_replica_settings.items() %} + IF NOT EXISTS ( + SELECT replica_server_name, {{ value.sys_setting_name }} + FROM sys.availability_replicas + WHERE replica_server_name = N'{{ hostvars[item]['ansible_hostname'] }}' + AND +{% if key == 'endpoint_url' %} + {{ value.sys_setting_name }} = {{ value.setting_value }} +{% else %} + {{ value.sys_setting_name }} = '{{ value.setting_value }}' +{% endif %} + ) + BEGIN + ALTER AVAILABILITY GROUP {{ mssql_ha_ag_name }} MODIFY REPLICA ON + N'{{ hostvars[item]['ansible_hostname'] }}' WITH ( +{% if key == 'allow_connections' %} + {{ value.sql_setting_name }} +{% else %} + {{ value.sql_setting_name }} = {{ value.setting_value }} +{% endif %} + ); + PRINT '{{ hostvars[item]['ansible_hostname'] }}: \ +The {{ value.sql_setting_name }} setting on this \ +{{ hostvars[item]['mssql_ha_replica_type'] }} replica configured successfully'; + END + ELSE + BEGIN + PRINT '{{ hostvars[item]['ansible_hostname'] }}: \ +The {{ value.sql_setting_name }} setting on this \ +{{ hostvars[item]['mssql_ha_replica_type'] }} replica is already set \ +correctly, skipping'; + END +{% endfor %} + END +{% endif %} +{% endfor %} +END diff --git a/tests/templates/alter_endpoint.j2 b/tests/templates/alter_endpoint.j2 new file mode 100644 index 00000000..71d4a2ba --- /dev/null +++ b/tests/templates/alter_endpoint.j2 @@ -0,0 +1,24 @@ +IF NOT EXISTS( + SELECT name + FROM sys.certificates + WHERE name = 'test_cert' +) +BEGIN + PRINT 'Certificate test_cert does not exist, creating'; + CREATE CERTIFICATE test_cert + WITH SUBJECT = 'Test certificate'; + PRINT 'Certificate test_cert created successfully'; +END +ELSE +BEGIN + PRINT 'Certificate test_cert already exists, skipping'; +END + +ALTER ENDPOINT {{ mssql_ha_endpoint_name }} + STATE = STOPPED + AS TCP (LISTENER_PORT = 1234) + FOR DATABASE_MIRRORING ( + ROLE = PARTNER, + AUTHENTICATION = CERTIFICATE test_cert, + ENCRYPTION = REQUIRED ALGORITHM RC4 + ); diff --git a/tests/templates/configure_ag_cluster_type_none.j2 b/tests/templates/configure_ag_cluster_type_none.j2 new file mode 100644 index 00000000..2bd3533b --- /dev/null +++ b/tests/templates/configure_ag_cluster_type_none.j2 @@ -0,0 +1,10 @@ +CREATE AVAILABILITY GROUP {{ mssql_ha_ag_name }} + WITH (CLUSTER_TYPE = NONE) + FOR REPLICA ON + N'{{ ansible_hostname }}' WITH ( + ENDPOINT_URL = N'tcp://{{ ansible_fqdn }}:{{ mssql_ha_listener_port }}', + AVAILABILITY_MODE = SYNCHRONOUS_COMMIT, + FAILOVER_MODE = MANUAL, + SEEDING_MODE = AUTOMATIC, + SECONDARY_ROLE (ALLOW_CONNECTIONS = ALL) + ); diff --git a/tests/templates/create_example_db.j2 b/tests/templates/create_example_db.j2 new file mode 100644 index 00000000..9b441edb --- /dev/null +++ b/tests/templates/create_example_db.j2 @@ -0,0 +1,38 @@ +IF NOT EXISTS( + SELECT name + FROM sys.databases + WHERE name = '{{ __mssql_test_db_name }}' +) +BEGIN + PRINT 'Creating the {{ __mssql_test_db_name }} database'; + CREATE DATABASE {{ __mssql_test_db_name }}; + PRINT 'The {{ __mssql_test_db_name }} database created successfully'; +END +ELSE +BEGIN + PRINT 'The {{ __mssql_test_db_name }} database already exists, skipping'; +END +GO + +USE {{ __mssql_test_db_name }}; +GO + +IF NOT EXISTS ( + SELECT name, xtype + FROM sysobjects + WHERE name='Inventory' and xtype='U' +) +BEGIN + PRINT 'Adding the Inventory table to the {{ __mssql_test_db_name }} database'; + CREATE TABLE Inventory (id INT, name NVARCHAR(50), quantity INT); + INSERT INTO Inventory VALUES (1, 'apple', 100); + INSERT INTO Inventory VALUES (2, 'orange', 150); + INSERT INTO Inventory VALUES (3, 'banana', 154); + INSERT INTO Inventory VALUES (4, N'バナナ', 170); + PRINT 'The Inventory table created successfully'; +END +ELSE +BEGIN + PRINT 'The Inventory table already exists, skipping'; +END +GO \ No newline at end of file diff --git a/tests/templates/disable_alwayson.j2 b/tests/templates/disable_alwayson.j2 new file mode 100644 index 00000000..ce0db8f5 --- /dev/null +++ b/tests/templates/disable_alwayson.j2 @@ -0,0 +1 @@ +ALTER EVENT SESSION AlwaysOn_health ON SERVER WITH (STARTUP_STATE=OFF); \ No newline at end of file diff --git a/tests/templates/drop_ag.j2 b/tests/templates/drop_ag.j2 new file mode 100644 index 00000000..d6194eed --- /dev/null +++ b/tests/templates/drop_ag.j2 @@ -0,0 +1,13 @@ +IF EXISTS ( + SELECT name + FROM sys.availability_groups + WHERE name = '{{ mssql_ha_ag_name }}' +) +BEGIN + DROP AVAILABILITY GROUP ag1; + PRINT 'The {{ mssql_ha_ag_name }} availability group dropped successfully'; +END +ELSE +BEGIN + PRINT 'The {{ mssql_ha_ag_name }} ag does not exist, skipping'; +END \ No newline at end of file diff --git a/tests/templates/drop_endpoint.j2 b/tests/templates/drop_endpoint.j2 new file mode 100644 index 00000000..a9485780 --- /dev/null +++ b/tests/templates/drop_endpoint.j2 @@ -0,0 +1,14 @@ +PRINT 'Dropping the {{ mssql_ha_endpoint_name }} endpoint' +IF NOT EXISTS ( + SELECT name + FROM sys.database_mirroring_endpoints + WHERE name = '{{ mssql_ha_endpoint_name }}' +) +BEGIN + PRINT 'Endpoint {{ mssql_ha_endpoint_name }} does not exist, skipping' +END +ELSE +BEGIN + DROP ENDPOINT {{ mssql_ha_endpoint_name }}; + PRINT 'Endpoint {{ mssql_ha_endpoint_name }} dropped successfully' +END \ No newline at end of file diff --git a/tests/templates/remove_sql_cert.j2 b/tests/templates/remove_sql_cert.j2 new file mode 100644 index 00000000..e30bef13 --- /dev/null +++ b/tests/templates/remove_sql_cert.j2 @@ -0,0 +1 @@ +DROP CERTIFICATE {{ mssql_ha_cert_name }}; \ No newline at end of file diff --git a/tests/templates/separate_from_ag.j2 b/tests/templates/separate_from_ag.j2 new file mode 100644 index 00000000..48f7223b --- /dev/null +++ b/tests/templates/separate_from_ag.j2 @@ -0,0 +1,2 @@ +DROP DATABASE {{ __mssql_test_database }} +DROP AVAILABILITY GROUP {{ mssql_ha_ag_name }} \ No newline at end of file diff --git a/tests/tests_configure_ha_cluster.yml b/tests/tests_configure_ha_cluster.yml new file mode 100644 index 00000000..f26be4e0 --- /dev/null +++ b/tests/tests_configure_ha_cluster.yml @@ -0,0 +1,715 @@ +# SPDX-License-Identifier: MIT +--- +- name: Verify the role on the primary node + hosts: all + vars: + mssql_accept_microsoft_odbc_driver_17_for_sql_server_eula: true + mssql_accept_microsoft_cli_utilities_for_sql_server_eula: true + mssql_accept_microsoft_sql_server_standard_eula: true + mssql_password: "p@55w0rD" + mssql_edition: Evaluation + mssql_ha_db_name: ExampleDB + __mssql_test_db_name: "{{ mssql_ha_db_name }}" + mssql_debug: true + tasks: + - name: Verify that by default the role fails on EL < 8 + when: + - ansible_distribution in ['CentOS', 'RedHat'] + - ansible_distribution_version is version('8', '<') + block: + - name: Run the role + vars: + mssql_ha_configure: true + include_role: + name: linux-system-roles.mssql + + - name: Unreachable task + fail: + msg: The above task must fail + rescue: + - name: Assert that the role failed with EL < 8 not supported + assert: + that: >- + 'mssql_ha_configure=true does not support running against EL 7 + hosts' in ansible_failed_result.msg + + - name: End EL < 8 host + meta: end_host + + - name: Set the mssql_ha_replica_type fact to appear in hostvars + set_fact: + mssql_ha_replica_type: primary + when: + - ansible_play_hosts_all | length == 1 + - mssql_ha_replica_type is not defined + + - name: Set up test environment for the ha_cluster role + include_role: + name: fedora.linux_system_roles.ha_cluster + tasks_from: test_setup.yml + + - name: Configure SQL Server and create an ExampleDB database on primary + vars: + mssql_input_sql_file: create_example_db.j2 + include_role: + name: linux-system-roles.mssql + when: mssql_ha_replica_type == 'primary' + + - name: Run on all hosts to configure HA cluster + vars: + mssql_ha_configure: true + mssql_ha_firewall_configure: true + mssql_ha_listener_port: 5022 + mssql_ha_cert_name: mssql_cert + mssql_ha_master_key_password: "p@55w0rD1" + mssql_ha_private_key_password: "p@55w0rD2" + mssql_ha_reset_cert: false + mssql_ha_endpoint_name: hadr_endpoint + mssql_ha_ag_name: ag1 + mssql_ha_db_backup_path: /var/opt/mssql/data/{{ mssql_ha_db_name }}.bak + mssql_ha_login: pacemakerLogin + mssql_ha_login_password: "p@55w0rD3" + mssql_ha_cluster_run_role: true + ha_cluster_cluster_name: "{{ mssql_ha_ag_name }}" + ha_cluster_hacluster_password: "p@55w0rD4" + ha_cluster_cluster_properties: + - attrs: + - name: cluster-recheck-interval + value: 2min + - name: start-failure-is-fatal + value: true + - name: stonith-enabled + value: true + ha_cluster_resource_primitives: + - id: Example_apc + agent: stonith:fence_apc_snmp + instance_attrs: + - attrs: + - name: login + value: apc_login + - name: passwd + value: apc_pass + - name: ipaddr + value: apc-switch.example.com + - name: pcmk_host_map + value: rhel8-node1.example.com:1;rhel8-node2.example.com:2 + - id: ag_cluster + agent: ocf:mssql:ag + instance_attrs: + - attrs: + - name: ag_name + value: "{{ mssql_ha_ag_name }}" + meta_attrs: + - attrs: + - name: failure-timeout + value: 60s + - id: virtualip + agent: ocf:heartbeat:IPaddr2 + instance_attrs: + - attrs: + - name: ip + value: 192.XXX.XXX.XXX + operations: + - action: monitor + attrs: + - name: interval + value: 30s + ha_cluster_resource_clones: + - resource_id: ag_cluster + promotable: yes + meta_attrs: + - attrs: + - name: notify + value: true + ha_cluster_constraints_colocation: + - resource_leader: + id: ag_cluster-clone + role: Promoted + resource_follower: + id: virtualip + options: + - name: score + value: INFINITY + ha_cluster_constraints_order: + - resource_first: + id: ag_cluster-clone + action: promote + resource_then: + id: virtualip + action: start + include_role: + name: linux-system-roles.mssql + +# This play tests if templates configure systems correctly +# Because CI runs on a single node, the following templates cannot be tested: +# remove_from_ag.j2, +# join_to_ag.j2, +# removing and adding replicas in configure_ag.j2 +- name: Test templates + hosts: all + vars: + mssql_accept_microsoft_odbc_driver_17_for_sql_server_eula: true + mssql_accept_microsoft_cli_utilities_for_sql_server_eula: true + mssql_accept_microsoft_sql_server_standard_eula: true + mssql_password: "p@55w0rD" + mssql_edition: Evaluation + mssql_ha_db_name: ExampleDB + __mssql_test_db_name: "{{ mssql_ha_db_name }}" + mssql_debug: true + mssql_ha_configure: true + mssql_ha_firewall_configure: true + mssql_ha_listener_port: 5022 + mssql_ha_cert_name: mssql_cert + mssql_ha_master_key_password: "p@55w0rD1" + mssql_ha_private_key_password: "p@55w0rD2" + mssql_ha_reset_cert: false + mssql_ha_endpoint_name: hadr_endpoint + mssql_ha_ag_name: ag1 + mssql_ha_db_backup_path: /var/opt/mssql/data/{{ mssql_ha_db_name }}.bak + mssql_ha_login: pacemakerLogin + mssql_ha_login_password: "p@55w0rD3" + mssql_ha_cluster_run_role: false + tasks: + - name: Set the mssql_ha_replica_type fact to appear in hostvars + set_fact: + mssql_ha_replica_type: primary + when: + - ansible_play_hosts_all | length == 1 + - mssql_ha_replica_type is not defined + + - name: Ensure ansible_facts and variables used by role + block: + - name: Include vars in a legacy role format + include_vars: ../vars/main.yml + rescue: + - name: Include vars in a collection format + include_vars: ../../roles/server/vars/main.yml + + # enable_alwayson.j2 template test + - name: Enable AlwaysOn event session when it's enabled + vars: + __mssql_input_sql_file: enable_alwayson.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Assert that enabling is skipped + assert: + that: >- + "AlwaysOn Health events already enabled, skipping" + in __mssql_sqlcmd_input_file.stdout + + - name: Disable AlwaysOn event session + vars: + __mssql_input_sql_file: disable_alwayson.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Enable AlwaysOn event session + vars: + __mssql_input_sql_file: enable_alwayson.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Assert that the template reported changed state + assert: + that: >- + "AlwaysOn Health events enabled successfully" + in __mssql_sqlcmd_input_file.stdout + + # configure_endpoint.j2 template test + - name: Create endpoint when it exists + vars: + __mssql_input_sql_file: configure_endpoint.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Assert expected messages + assert: + that: + - >- + "Verifying the existing endpoint {{ mssql_ha_endpoint_name }}" + in __mssql_sqlcmd_input_file.stdout + - >- + "The LISTENER_PORT setting for the {{ mssql_ha_endpoint_name }} + endpoint is already set to {{ mssql_ha_listener_port }}, skipping" + in __mssql_sqlcmd_input_file.stdout + - >- + "The ROLE setting for the {{ mssql_ha_endpoint_name }} endpoint is + already set to {{ __mssql_ha_endpoint_role }}, skipping" + in __mssql_sqlcmd_input_file.stdout + - >- + "The certificate for the {{ mssql_ha_endpoint_name }} + endpoint is already set to {{ mssql_ha_cert_name }}, skipping" + in __mssql_sqlcmd_input_file.stdout + - >- + "The ENCRYPTION setting for the {{ mssql_ha_endpoint_name }} + endpoint is already set to AES, skipping" + in __mssql_sqlcmd_input_file.stdout + - >- + "Endpoint {{ mssql_ha_endpoint_name }} is already started, skipping" + in __mssql_sqlcmd_input_file.stdout + + - name: Alter endpoint to test how template handles incorrect endpoint + vars: + __mssql_input_sql_file: alter_endpoint.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Configure endpoint correctly + vars: + __mssql_input_sql_file: configure_endpoint.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Assert expected messages + assert: + that: + - >- + "Verifying the existing endpoint {{ mssql_ha_endpoint_name }}" + in __mssql_sqlcmd_input_file.stdout + - >- + "The LISTENER_PORT setting for the {{ mssql_ha_endpoint_name }} + endpoint updated to {{ mssql_ha_listener_port }} successfully" + in __mssql_sqlcmd_input_file.stdout + - >- + "The ROLE setting for the {{ mssql_ha_endpoint_name }} + endpoint updated to {{ __mssql_ha_endpoint_role }} successfully" + in __mssql_sqlcmd_input_file.stdout + - >- + "The certificate for the {{ mssql_ha_endpoint_name }} + endpoint updated to {{ mssql_ha_cert_name }} successfully" + in __mssql_sqlcmd_input_file.stdout + - >- + "The ENCRYPTION setting for the {{ mssql_ha_endpoint_name }} + endpoint updated to AES successfully" + in __mssql_sqlcmd_input_file.stdout + - >- + "Endpoint {{ mssql_ha_endpoint_name }} started successfully" + in __mssql_sqlcmd_input_file.stdout + + - name: Drop the test_cert certificate + vars: + __mssql_input_sql_file: drop_cert.j2 + mssql_ha_cert_name: test_cert + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + # create_master_key_encryption.j2 template test + - name: Create a master key when it exists + vars: + __mssql_input_sql_file: create_master_key_encryption.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Assert that creating is skipped + assert: + that: >- + "The provided master key password is correct" + in __mssql_sqlcmd_input_file.stdout + + - name: Verify creating master key with incorrect password + block: + - name: Try creating master key with incorrect password + vars: + __mssql_input_sql_file: create_master_key_encryption.j2 + mssql_ha_master_key_password: "p@55w0rD11" + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: This task shouldn't execute + fail: + rescue: + - name: Assert the incorrect password error message + assert: + that: >- + "You provided an incorrect master key password" + in __mssql_sqlcmd_input_file.stdout + + - name: Drop endpoint + vars: + __mssql_input_sql_file: create_master_key_encryption.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Remove certificate from SQL Server to verify re-creating master key + vars: + __mssql_input_sql_file: drop_cert.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Remove certificate and private key + file: + path: "{{ item }}" + state: absent + loop: + - "{{ __mssql_ha_cert_dest }}" + - "{{ __mssql_ha_private_key_dest }}" + + - name: >- + Verify creating master key with incorrect password and + mssql_ha_reset_cert set to true + vars: + __mssql_input_sql_file: create_master_key_encryption.j2 + mssql_ha_master_key_password: "p@55w0rD11" + mssql_ha_reset_cert: true + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Assert the expected error message + assert: + that: + - >- + "dropping master key to re-create it" + in __mssql_sqlcmd_input_file.stdout + - >- + "Master key dropped successfully" + in __mssql_sqlcmd_input_file.stdout + - >- + "Master key created successfully" + in __mssql_sqlcmd_input_file.stdout + + # # restore_cert.j2 template test + # - name: Create a certificate when it exists on secondary + # when: mssql_ha_replica_type in ['synchronous', 'witness'] + # block: + # - name: Input restore_cert.j2 + # vars: + # __mssql_input_sql_file: restore_cert.j2 + # include_role: + # name: linux-system-roles.mssql + # tasks_from: input_sql_file.yml + + # - name: Assert the already exists message + # assert: + # that: >- + # "Certificate {{ mssql_ha_cert_name }} already exists, skipping" + # in __mssql_sqlcmd_input_file.stdout + + # create_and_back_up_cert.j2 template test + - name: Create and back up an existing and backed up certificate + when: mssql_ha_replica_type == 'primary' + block: + - name: Input create_and_back_up_cert.j2 to create cert + vars: + __mssql_input_sql_file: create_and_back_up_cert.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Assert created successfully messages + assert: + that: + - >- + "Certificate {{ mssql_ha_cert_name }} created successfully" + in __mssql_sqlcmd_input_file.stdout + - >- + "{{ __mssql_ha_cert_dest }} and + {{ __mssql_ha_private_key_dest }} exported successfully" + in __mssql_sqlcmd_input_file.stdout + + - name: Input create_and_back_up_cert.j2 when cert exists + vars: + __mssql_input_sql_file: create_and_back_up_cert.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Assert task skipping messages + assert: + that: + - >- + "Certificate {{ mssql_ha_cert_name }} already exists, skipping" + in __mssql_sqlcmd_input_file.stdout + - >- + "{{ __mssql_ha_cert_dest }} and + {{ __mssql_ha_private_key_dest }} already exist, skipping" + in __mssql_sqlcmd_input_file.stdout + + - name: Back up certificate and private key + copy: + remote_src: true + src: "{{ item }}" + dest: /tmp/{{ item | basename }} + loop: + - "{{ __mssql_ha_cert_dest }}" + - "{{ __mssql_ha_private_key_dest }}" + + - name: Remove private key + file: + path: "{{ __mssql_ha_private_key_dest }}" + state: absent + + - name: Input create_and_back_up_cert.j2 + vars: + __mssql_input_sql_file: create_and_back_up_cert.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Assert expected error messages + assert: + that: + - >- + "Certificate {{ mssql_ha_cert_name }} already exists, skipping" + in __mssql_sqlcmd_input_file.stdout + - >- + "{{ __mssql_ha_private_key_dest }} does not exist while + {{ __mssql_ha_cert_dest }} exists." + in __mssql_sqlcmd_input_file.stdout + + - name: Remove certificate + file: + path: "{{ __mssql_ha_cert_dest }}" + state: absent + + - name: Return private key + copy: + remote_src: true + src: /tmp/{{ __mssql_ha_private_key_dest | basename }} + dest: "{{ __mssql_ha_private_key_dest }}" + + - name: Input create_and_back_up_cert.j2 + vars: + __mssql_input_sql_file: create_and_back_up_cert.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Assert expected error messages + assert: + that: + - >- + "Certificate {{ mssql_ha_cert_name }} already exists, skipping" + in __mssql_sqlcmd_input_file.stdout + - >- + "{{ __mssql_ha_cert_dest }} does not exist while + {{ __mssql_ha_private_key_dest }} exists." + in __mssql_sqlcmd_input_file.stdout + + - name: Return certificate + copy: + remote_src: true + src: /tmp/{{ __mssql_ha_cert_dest | basename }} + dest: "{{ __mssql_ha_cert_dest }}" + + - name: Drop certificate from SQL Server + vars: + __mssql_input_sql_file: drop_cert.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Create cert when cert files exist and SQL cert does not + block: + - name: Input create_and_back_up_cert.j2 + vars: + __mssql_input_sql_file: create_and_back_up_cert.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Unreachable task + fail: + msg: The above task must fail + rescue: + - name: Assert expected error messages + assert: + that: + - >- + "Certificate {{ mssql_ha_cert_name }} does not exist in SQL + Server, however, {{ __mssql_ha_cert_dest }} and/or + {{ __mssql_ha_private_key_dest }} files do exist." + in __mssql_sqlcmd_input_file.stdout + + - name: Remove certificate and private key + file: + path: "{{ item }}" + state: absent + loop: + - "{{ __mssql_ha_cert_dest }}" + - "{{ __mssql_ha_private_key_dest }}" + + - name: Ensure that certificates exist + vars: + __mssql_input_sql_file: create_and_back_up_cert.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Assert expected messages + assert: + that: >- + "Certificate {{ mssql_ha_cert_name }} created successfully" + in __mssql_sqlcmd_input_file.stdout + + # create_ha_login.j2 template test + - name: Create a {{ mssql_ha_login }} login when it already exists + vars: + __mssql_input_sql_file: create_ha_login.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Assert task skipping messages + assert: + that: + - >- + "A {{ mssql_ha_login }} login already exists, skipping" + in __mssql_sqlcmd_input_file.stdout + - >- + "{{ mssql_ha_login }} is a member of sysadmin role, skipping" + in __mssql_sqlcmd_input_file.stdout + + # replicate_db.j2 template test + - name: Replicate database what it is already replicated + when: mssql_ha_replica_type == 'primary' + block: + - name: Input replicate_db.j2 + vars: + __mssql_input_sql_file: replicate_db.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Assert task skipping messages + assert: + that: + - >- + "RECOVERY FULL on the {{ mssql_ha_db_name }} database is set, + skipping" in __mssql_sqlcmd_input_file.stdout + - >- + "The {{ mssql_ha_db_name }} database is already backed up, + skipping" in __mssql_sqlcmd_input_file.stdout + - >- + "database is already added to the {{ mssql_ha_ag_name }} + availability group, skipping" + in __mssql_sqlcmd_input_file.stdout + + # configure_ag.yml template test + - name: Ensure endpoint exists + vars: + __mssql_input_sql_file: configure_endpoint.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Test configure_ag.yml on primary + when: mssql_ha_replica_type == 'primary' + block: + - name: Input configure_ag.yml when AG exists + vars: + __mssql_input_sql_file: configure_ag.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Assert task skipping messages + assert: + that: + - >- + "DB_FAILOVER = ON is already set, skipping" + in __mssql_sqlcmd_input_file.stdout + - >- + "The ENDPOINT_URL setting on this {{ mssql_ha_replica_type }} + replica is already set correctly, skipping" + in __mssql_sqlcmd_input_file.stdout + - >- + "The FAILOVER_MODE setting on this {{ mssql_ha_replica_type }} + replica is already set correctly, skipping" + in __mssql_sqlcmd_input_file.stdout + - >- + "The SEEDING_MODE setting on this {{ mssql_ha_replica_type }} + replica is already set correctly, skipping" + in __mssql_sqlcmd_input_file.stdout + - >- + "The SECONDARY_ROLE (ALLOW_CONNECTIONS = ALL) setting on this + {{ mssql_ha_replica_type }} replica is already set correctly" + in __mssql_sqlcmd_input_file.stdout + + # Using mssql_ha_ag_name: ag2 because running tasks against ag1 fails + # with "Replica not Primary" + - name: Verify configuration of a new ag2 + vars: + __mssql_input_sql_file: configure_ag_cluster_type_none.j2 + mssql_ha_ag_name: ag2 + block: + - name: Configure AG with CLUSTER_TYPE = NONE + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Input configure_ag.yml with CLUSTER_TYPE = NONE + vars: + __mssql_input_sql_file: configure_ag.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Assert task skipping messages + assert: + that: + - >- + "The existing {{ mssql_ha_ag_name }} availability group has + incorrect cluster type set, dropping the group to re-create + it" + in __mssql_sqlcmd_input_file.stdout + - >- + "The {{ mssql_ha_ag_name }} availability group dropped + successfully" + in __mssql_sqlcmd_input_file.stdout + - >- + "The {{ mssql_ha_ag_name }} availability group created + successfully" + in __mssql_sqlcmd_input_file.stdout + + - name: Alter AG + vars: + __mssql_input_sql_file: alter_ag.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Input configure_ag.yml to configure ag properly + vars: + __mssql_input_sql_file: configure_ag.j2 + include_role: + name: linux-system-roles.mssql + tasks_from: input_sql_file.yml + + - name: Assert task skipping messages + assert: + that: + - >- + "Verifying the existing availability group + {{ mssql_ha_ag_name }}" + in __mssql_sqlcmd_input_file.stdout + - >- + "The ENDPOINT_URL setting on this + {{ mssql_ha_replica_type }} replica configured successfully" + in __mssql_sqlcmd_input_file.stdout + - >- + "The FAILOVER_MODE setting on this + {{ mssql_ha_replica_type }} replica is already set + correctly, skipping" + in __mssql_sqlcmd_input_file.stdout + - >- + "The SEEDING_MODE setting on this + {{ mssql_ha_replica_type }} replica configured successfully" + in __mssql_sqlcmd_input_file.stdout + - >- + "The SECONDARY_ROLE (ALLOW_CONNECTIONS = ALL) setting on + this {{ mssql_ha_replica_type }} replica configured + successfully" + in __mssql_sqlcmd_input_file.stdout diff --git a/vars/main.yml b/vars/main.yml index 68ae5814..120248ab 100644 --- a/vars/main.yml +++ b/vars/main.yml @@ -10,3 +10,37 @@ __mssql_client_packages: [mssql-tools, unixODBC-devel] __mssql_server_fts_packages: mssql-server-fts __mssql_server_ha_packages: mssql-server-ha __mssql_powershell_packages: powershell +__mssql_ha_endpoint_role: >- + {%- if mssql_ha_replica_type in ['primary', 'synchronous'] -%} + ALL + {%- elif mssql_ha_replica_type == 'witness' -%} + WITNESS + {%- else -%} + null + {%- endif -%} +__mssql_ha_availability_mode: >- + {%- if mssql_ha_replica_type in ['primary', 'synchronous'] -%} + SYNCHRONOUS_COMMIT + {%- elif mssql_ha_replica_type == 'witness' -%} + CONFIGURATION_ONLY + {%- else -%} + null + {%- endif -%} +__mssql_ha_failover_mode: >- + {%- if mssql_ha_replica_type in ['primary', 'synchronous'] -%} + EXTERNAL + {%- elif mssql_ha_replica_type == 'witness' -%} + MANUAL + {%- else -%} + null + {%- endif -%} +__mssql_ha_seeding_mode: >- + {%- if mssql_ha_replica_type in ['primary', 'synchronous'] -%} + AUTOMATIC + {%- elif mssql_ha_replica_type == 'witness' -%} + MANUAL + {%- else -%} + null + {%- endif -%} +__mssql_ha_cert_dest: /var/opt/mssql/data/{{ mssql_ha_cert_name }}.cer +__mssql_ha_private_key_dest: /var/opt/mssql/data/{{ mssql_ha_cert_name }}.pvk