From bcb5cb4e23395ca0040a1030df0b5a02e7527e6f Mon Sep 17 00:00:00 2001 From: Arnav Singh Date: Thu, 12 Oct 2017 17:21:04 -0700 Subject: [PATCH] Added sample Ansible playbook for installing SQL Server and creating a Pacemaker-managed AG. --- .../Linux/Ansible Playbook/README.md | 57 ++++ .../Linux/Ansible Playbook/inventory | 4 + .../Ansible Playbook/library/mssql_ag.py | 253 ++++++++++++++++++ .../library/mssql_ag_listener.py | 184 +++++++++++++ .../library/mssql_certificate.py | 169 ++++++++++++ .../Ansible Playbook/library/mssql_conf.py | 138 ++++++++++ .../library/mssql_endpoint.py | 201 ++++++++++++++ .../library/mssql_grant_endpoint.py | 140 ++++++++++ .../Ansible Playbook/library/mssql_login.py | 160 +++++++++++ .../library/mssql_master_key.py | 123 +++++++++ .../Ansible Playbook/library/mssql_user.py | 136 ++++++++++ .../Linux/Ansible Playbook/play.yml | 75 ++++++ .../mssql-server-ag-external/meta/main.yml | 5 + .../mssql-server-ag-external/tasks/main.yml | 94 +++++++ .../roles/mssql-server-ha/meta/main.yml | 4 + .../roles/mssql-server-ha/tasks/main.yml | 118 ++++++++ .../roles/mssql-server/tasks/main.yml | 102 +++++++ .../roles/pacemaker/tasks/main.yml | 96 +++++++ 18 files changed, 2059 insertions(+) create mode 100644 samples/features/high availability/Linux/Ansible Playbook/README.md create mode 100644 samples/features/high availability/Linux/Ansible Playbook/inventory create mode 100644 samples/features/high availability/Linux/Ansible Playbook/library/mssql_ag.py create mode 100644 samples/features/high availability/Linux/Ansible Playbook/library/mssql_ag_listener.py create mode 100644 samples/features/high availability/Linux/Ansible Playbook/library/mssql_certificate.py create mode 100644 samples/features/high availability/Linux/Ansible Playbook/library/mssql_conf.py create mode 100644 samples/features/high availability/Linux/Ansible Playbook/library/mssql_endpoint.py create mode 100644 samples/features/high availability/Linux/Ansible Playbook/library/mssql_grant_endpoint.py create mode 100644 samples/features/high availability/Linux/Ansible Playbook/library/mssql_login.py create mode 100644 samples/features/high availability/Linux/Ansible Playbook/library/mssql_master_key.py create mode 100644 samples/features/high availability/Linux/Ansible Playbook/library/mssql_user.py create mode 100644 samples/features/high availability/Linux/Ansible Playbook/play.yml create mode 100644 samples/features/high availability/Linux/Ansible Playbook/roles/mssql-server-ag-external/meta/main.yml create mode 100644 samples/features/high availability/Linux/Ansible Playbook/roles/mssql-server-ag-external/tasks/main.yml create mode 100644 samples/features/high availability/Linux/Ansible Playbook/roles/mssql-server-ha/meta/main.yml create mode 100644 samples/features/high availability/Linux/Ansible Playbook/roles/mssql-server-ha/tasks/main.yml create mode 100644 samples/features/high availability/Linux/Ansible Playbook/roles/mssql-server/tasks/main.yml create mode 100644 samples/features/high availability/Linux/Ansible Playbook/roles/pacemaker/tasks/main.yml diff --git a/samples/features/high availability/Linux/Ansible Playbook/README.md b/samples/features/high availability/Linux/Ansible Playbook/README.md new file mode 100644 index 00000000..cbf135b3 --- /dev/null +++ b/samples/features/high availability/Linux/Ansible Playbook/README.md @@ -0,0 +1,57 @@ +This is a sample Ansible playbook that shows how to install SQL Server, create a Pacemaker cluster, and create an AG managed by the cluster on a set of Linux nodes. + + +# Roles + +- `pacemaker` - This role creates a Pacemaker cluster between the hosts. +- `mssql-server` - This role installs SQL Server on the host, runs setup to set the SA password, and starts the service. +- `mssql-server-ha` - This role enables support for HA and creates a DB Mirroring endpoint. +- `mssql-server-ag-external` - This role installs the Pacemaker resource agents, creates an AG, an optional listener, and Pacemaker resources for both. + + +# Try + +1. Put the names of the Linux nodes in the `inventory` file + +1. Configure the deployment in `play.yml` + +1. Create a vault file named `vault.yml` using the template at the end of this README. + + ```sh + ansible-vault create vault.yml + ``` + +1. Execute the playbook + + ```sh + ansible-playbook ./play.yml -i ./inventory --ask-vault-pass -e 'ansible_user=username' + ``` + + +# Vault file template + +```yaml +--- + +ansible_ssh_pass: 'some password' + +ansible_sudo_pass: 'some password' + +# The password for the sa user. Only used if mssql-server needs to be installed. +sa_password: 'some password' + +# The password for the master key +master_key_password: 'some password' + +# The SQL password for the DBM endpoint user +dbm_password: 'some password' + +# The password for the DBM cert private key +dbm_cert_password: 'some password' + +# The password of the user that admins the pacemaker cluster (hacluster) +pacemaker_cluster_password: 'some password' + +# The SQL password for the pacemaker user +pacemaker_password: 'some password' +``` diff --git a/samples/features/high availability/Linux/Ansible Playbook/inventory b/samples/features/high availability/Linux/Ansible Playbook/inventory new file mode 100644 index 00000000..0ea8986d --- /dev/null +++ b/samples/features/high availability/Linux/Ansible Playbook/inventory @@ -0,0 +1,4 @@ +[servers] +node1 +node2 +node3 diff --git a/samples/features/high availability/Linux/Ansible Playbook/library/mssql_ag.py b/samples/features/high availability/Linux/Ansible Playbook/library/mssql_ag.py new file mode 100644 index 00000000..87bb6649 --- /dev/null +++ b/samples/features/high availability/Linux/Ansible Playbook/library/mssql_ag.py @@ -0,0 +1,253 @@ +#!/usr/bin/python + +# Copyright (c) 2017 Microsoft Corporation + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'supported_by': 'community', + 'status': ['preview'] +} + +DOCUMENTATION = ''' +--- +module: mssql_ag + +short_description: Add or join availability groups on a SQL Server instance + +description: + - Add or join availability groups on a SQL Server instance. + +version_added: "2.2" + +author: Arnav Singh (@arsing) + +options: + name: + description: + - The name of the availability group to add + required: true + + state: + description: + - The state to set the local replica to + choices: ["all_secondaries_or_unjoined", "all_joined_to_one_primary"] + required: true + + all_replicas: + description: + - A list of all the replicas of the AG + required: false + + primary: + description: + - The replica that should become the primary + required: false + + local_replica: + description: + - The name of the local replica + required: false + + dbm_endpoint_port: + description: + - The port of the DBM endpoint + required: false + + login_port: + description: + - The TDS port of the instance + required: false + default: 1433 + + login_name: + description: + - The name of the user to log in to the instance + required: true + + login_password: + description: + - The password of the user to log in to the instance + required: true + +notes: + - Requires the mssql-tools package on the remote host. + +requirements: + - python >= 2.7 + - mssql-tools +'''.replace('\t', ' ') + +EXAMPLES = ''' +# Set all replicas of AG foo to secondary +- mssql_ag: + name: foo + state: all_secondaries_or_unjoined + login_name: sa + login_password: password + +# Join all replicas of AG foo to primary on the first server in the group named servers +- mssql_ag: + name: foo + state: all_joined_to_one_primary + all_replicas: "{{ groups['servers'] }}" + primary: "{{ groups['servers'][0] }}" + local_replica: "{{ inventory_hostname }}" + login_name: sa + login_password: password +'''.replace('\t', ' ') + +RETURN = ''' +name: + description: The name of the AG that was created or joined + returned: success + type: string + sample: foo +'''.replace('\t', ' ') + + +from ansible.module_utils.basic import AnsibleModule +import subprocess + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(required = True), + state = dict(choices = ['all_secondaries_or_unjoined', 'all_joined_to_one_primary'], required = True), + all_replicas = dict(type = 'list', required = False), + primary = dict(required = False), + local_replica = dict(required = False), + dbm_endpoint_port = dict(required = False), + login_port = dict(required = False, default = 1433), + login_name = dict(required = True), + login_password = dict(required = True, no_log = True) + ), + required_if = [ + ['state', 'all_joined_to_one_primary', ['all_replicas', 'primary', 'local_replica', 'dbm_endpoint_port']] + ] + ) + + name = module.params['name'] + state = module.params['state'] + all_replicas = module.params['all_replicas'] + primary = module.params['primary'] + local_replica = module.params['local_replica'] + dbm_endpoint_port = module.params['dbm_endpoint_port'] + login_port = module.params['login_port'] + login_name = module.params['login_name'] + login_password = module.params['login_password'] + + if state == "all_secondaries_or_unjoined": + sqlcmd(login_port, login_name, login_password, """ + IF EXISTS ( + SELECT * FROM sys.availability_groups WHERE name = {0} + ) + ALTER AVAILABILITY GROUP {1} SET (ROLE = SECONDARY) + ; + """.format( + quoteName(name, "'"), + quoteName(name, '[') + )) + + elif primary == local_replica: + def replica_spec(name, endpoint_port): + return """ + {0} WITH ( + ENDPOINT_URL = {1}, + AVAILABILITY_MODE = SYNCHRONOUS_COMMIT, + FAILOVER_MODE = EXTERNAL, + SEEDING_MODE = AUTOMATIC + ) + """.format( + quoteName(name.split('.')[0], "'"), + quoteName('tcp://{0}:{1}'.format(name, endpoint_port), "'") + ) + + sqlcmd(login_port, login_name, login_password, """ + IF NOT EXISTS ( + SELECT * FROM sys.availability_groups WHERE name = {0} + ) + CREATE AVAILABILITY GROUP {1} + WITH (CLUSTER_TYPE = EXTERNAL, DB_FAILOVER = ON) + FOR REPLICA ON {2} + ELSE IF NOT EXISTS ( + SELECT * + FROM sys.dm_hadr_availability_replica_states ars + JOIN sys.availability_groups ag ON ars.group_id = ag.group_id + WHERE ag.name = {0} AND ars.is_local = 1 AND ars.role = 1 + ) + BEGIN + EXEC sp_set_session_context @key = N'external_cluster', @value = N'yes', @read_only = 1 + ALTER AVAILABILITY GROUP {1} FAILOVER + END + ; + + ALTER AVAILABILITY GROUP {1} GRANT CREATE ANY DATABASE + ; + """.format( + quoteName(name, "'"), + quoteName(name, '['), + replica_spec(primary, dbm_endpoint_port) + )) + + for replica in all_replicas: + if replica != primary: + sqlcmd(login_port, login_name, login_password, """ + IF NOT EXISTS ( + SELECT * + FROM sys.availability_replicas ar + JOIN sys.availability_groups ag ON ar.group_id = ag.group_id + WHERE ag.name = {0} AND ar.replica_server_name = {2} + ) + ALTER AVAILABILITY GROUP {1} + ADD REPLICA ON {3} + ; + """.format( + quoteName(name, "'"), + quoteName(name, '['), + quoteName(replica.split('.')[0], "'"), + replica_spec(replica, dbm_endpoint_port) + )) + + else: + sqlcmd(login_port, login_name, login_password, """ + IF NOT EXISTS ( + SELECT * FROM sys.availability_groups WHERE name = {0} + ) + ALTER AVAILABILITY GROUP {1} JOIN WITH (CLUSTER_TYPE = EXTERNAL) + ; + + ALTER AVAILABILITY GROUP {1} GRANT CREATE ANY DATABASE + ; + """.format( + quoteName(name, "'"), + quoteName(name, '[') + )) + + module.exit_json(changed = True, name = name) + +def sqlcmd(login_port, login_name, login_password, command): + subprocess.check_call([ + '/opt/mssql-tools/bin/sqlcmd', + '-S', + "localhost,{0}".format(login_port), + '-U', + login_name, + '-P', + login_password, + '-b', + '-Q', + command + ]) + +def quoteName(name, quote_char): + if quote_char == '[' or quote_char == ']': + (quote_start_char, quote_end_char) = ('[', ']') + elif quote_char == "'": + (quote_start_char, quote_end_char) = ("N'", "'") + else: + raise Exception("Unsupported quote_char {0}, must be [ or ] or '".format(quote_char)) + + return "{0}{1}{2}".format(quote_start_char, name.replace(quote_end_char, quote_end_char + quote_end_char), quote_end_char) + +if __name__ == '__main__': + main() diff --git a/samples/features/high availability/Linux/Ansible Playbook/library/mssql_ag_listener.py b/samples/features/high availability/Linux/Ansible Playbook/library/mssql_ag_listener.py new file mode 100644 index 00000000..1452b966 --- /dev/null +++ b/samples/features/high availability/Linux/Ansible Playbook/library/mssql_ag_listener.py @@ -0,0 +1,184 @@ +#!/usr/bin/python + +# Copyright (c) 2017 Microsoft Corporation + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'supported_by': 'community', + 'status': ['preview'] +} + +DOCUMENTATION = ''' +--- +module: mssql_ag_listener + +short_description: Create an availability group listener on a SQL Server instance + +description: + - Create an availability group listener on a SQL Server instance. + +version_added: "2.2" + +author: Arnav Singh (@arsing) + +options: + name: + description: + - The name of the listener to add + required: true + + ag_name: + description: + - The name of the availability group to add the listener to + required: true + + ip: + description: + - The IPs for the listener to bind to + required: true + + readonly_routing_replicas: + description: + - A list of all the replicas of the AG that should participate in read-only routing + required: false + + login_port: + description: + - The TDS port of the instance + required: false + default: 1433 + + login_name: + description: + - The name of the user to log in to the instance + required: true + + login_password: + description: + - The password of the user to log in to the instance + required: true + +notes: + - Requires the mssql-tools package on the remote host. + +requirements: + - python >= 2.7 + - mssql-tools +'''.replace('\t', ' ') + +EXAMPLES = ''' +# Creates an AG listener named foo for the AG named bar with IP 1.2.3.4, and have all replicas in the "servers" group participate in read-only routing +- mssql_ag_listener: + name: foo + ag_name: bar + ip: + - '1.2.3.4' + readonly_routing_replicas: "{{ groups['servers'] }}" + login_name: sa + login_password: password +'''.replace('\t', ' ') + +RETURN = ''' +name: + description: The name of the AG listener that was created + returned: success + type: string + sample: foo +'''.replace('\t', ' ') + + +from ansible.module_utils.basic import AnsibleModule +import subprocess + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(required = True), + ag_name = dict(required = True), + ip = dict(type = 'list', required = True), + readonly_routing_replicas = dict(type = 'list', required = True), + login_port = dict(required = False, default = 1433), + login_name = dict(required = True), + login_password = dict(required = True, no_log = True) + ) + ) + + name = module.params['name'] + ag_name = module.params['ag_name'] + ips = module.params['ip'] + readonly_routing_replicas = module.params['readonly_routing_replicas'] + login_port = module.params['login_port'] + login_name = module.params['login_name'] + login_password = module.params['login_password'] + + sqlcmd(login_port, login_name, login_password, """ + IF EXISTS ( + SELECT * + FROM + sys.availability_groups ag JOIN + sys.availability_group_listeners agl ON ag.group_id = agl.group_id + WHERE + ag.name = {0} AND agl.dns_name = {2} + ) + ALTER AVAILABILITY GROUP {1} REMOVE LISTENER {2} + ; + + ALTER AVAILABILITY GROUP {1} ADD LISTENER {2} (WITH IP ({3})) + """.format( + quoteName(ag_name, "'"), + quoteName(ag_name, '['), + quoteName(name, "'"), + ', '.join("({0}, '255.255.255.255')".format(quoteName(ip, "'")) for ip in ips) + )) + + for replica in readonly_routing_replicas: + sqlcmd(login_port, login_name, login_password, """ + ALTER AVAILABILITY GROUP {0} MODIFY REPLICA ON {1} WITH (PRIMARY_ROLE (ALLOW_CONNECTIONS = READ_WRITE)) + ; + ALTER AVAILABILITY GROUP {0} MODIFY REPLICA ON {1} WITH (SECONDARY_ROLE (ALLOW_CONNECTIONS = READ_ONLY)) + ; + ALTER AVAILABILITY GROUP {0} MODIFY REPLICA ON {1} WITH (SECONDARY_ROLE (READ_ONLY_ROUTING_URL = {2})) + ; + """.format( + quoteName(ag_name, '['), + quoteName(replica.split('.')[0], "'"), + quoteName('tcp://{0}:{1}'.format(replica, login_port), "'") + )) + + for replica in readonly_routing_replicas: + sqlcmd(login_port, login_name, login_password, """ + ALTER AVAILABILITY GROUP {0} MODIFY REPLICA ON {1} WITH (PRIMARY_ROLE (READ_ONLY_ROUTING_LIST = ({2}))) + """.format( + quoteName(ag_name, '['), + quoteName(replica.split('.')[0], "'"), + ', '.join(quoteName(other_replica.split('.')[0], "'") for other_replica in readonly_routing_replicas if other_replica != replica) + )) + + module.exit_json(changed = True, name = name) + +def sqlcmd(login_port, login_name, login_password, command): + subprocess.check_call([ + '/opt/mssql-tools/bin/sqlcmd', + '-S', + "localhost,{0}".format(login_port), + '-U', + login_name, + '-P', + login_password, + '-b', + '-Q', + command + ]) + +def quoteName(name, quote_char): + if quote_char == '[' or quote_char == ']': + (quote_start_char, quote_end_char) = ('[', ']') + elif quote_char == "'": + (quote_start_char, quote_end_char) = ("N'", "'") + else: + raise Exception("Unsupported quote_char {0}, must be [ or ] or '".format(quote_char)) + + return "{0}{1}{2}".format(quote_start_char, name.replace(quote_end_char, quote_end_char + quote_end_char), quote_end_char) + +if __name__ == '__main__': + main() diff --git a/samples/features/high availability/Linux/Ansible Playbook/library/mssql_certificate.py b/samples/features/high availability/Linux/Ansible Playbook/library/mssql_certificate.py new file mode 100644 index 00000000..d4b6ca71 --- /dev/null +++ b/samples/features/high availability/Linux/Ansible Playbook/library/mssql_certificate.py @@ -0,0 +1,169 @@ +#!/usr/bin/python + +# Copyright (c) 2017 Microsoft Corporation + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'supported_by': 'community', + 'status': ['preview'] +} + +DOCUMENTATION = ''' +--- +module: mssql_certificate + +short_description: Add certificates to a SQL Server instance + +description: + - Add certificates to a SQL Server instance. + +version_added: "2.2" + +author: Arnav Singh (@arsing) + +options: + name: + description: + - The name of the certificate to add + required: true + + authorization_username: + description: + - The name of the SQL user to authorize with this certificate + required: false + default: 0.0.0.0 + + pub_key_path: + description: + - The path of the public key of the certificate (in Windows form) + required: true + + priv_key_path: + description: + - The path of the private key of the certificate (in Windows form) + required: true + + priv_key_password: + description: + - The password to decrypt the private key of the certificate + required: true + + login_port: + description: + - The TDS port of the instance + required: false + default: 1433 + + login_name: + description: + - The name of the user to log in to the instance + required: true + + login_password: + description: + - The password of the user to log in to the instance + required: true + +notes: + - Requires the mssql-tools package on the remote host. + +requirements: + - python >= 2.7 + - mssql-tools +'''.replace('\t', ' ') + +EXAMPLES = ''' +- mssql_certificate: + name: dbm_cert + authorization_username: dbm_user + pub_key_path: "C:\\var\\opt\\mssql\\secrets\\dbm_certificate.cer" + priv_key_path: "C:\\var\\opt\\mssql\\secrets\\dbm_certificate.pvk" + priv_key_password: password + login_name: sa + login_password: password +'''.replace('\t', ' ') + +RETURN = ''' +name: + description: The name of the certificate that was added + returned: success + type: string + sample: foo +'''.replace('\t', ' ') + + +from ansible.module_utils.basic import AnsibleModule +import subprocess + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(required = True), + authorization_username = dict(required = True), + pub_key_path = dict(required = True), + priv_key_path = dict(required = True), + priv_key_password = dict(required = True, no_log = True), + login_port = dict(required = False, default = 1433), + login_name = dict(required = True), + login_password = dict(required = True, no_log = True) + ) + ) + + name = module.params['name'] + authorization_username = module.params['authorization_username'] + pub_key_path = module.params['pub_key_path'] + priv_key_path = module.params['priv_key_path'] + priv_key_password = module.params['priv_key_password'] + login_port = module.params['login_port'] + login_name = module.params['login_name'] + login_password = module.params['login_password'] + + sqlcmd(login_port, login_name, login_password, """ + IF NOT EXISTS( + SELECT * FROM sys.certificates WHERE name = {0} + ) + CREATE CERTIFICATE {1} + AUTHORIZATION {2} + FROM FILE = {3} + WITH PRIVATE KEY ( + FILE = {4}, + DECRYPTION BY PASSWORD = {5} + ) + ; + """.format( + quoteName(name, "'"), + quoteName(name, '['), + quoteName(authorization_username, '['), + quoteName(pub_key_path, "'"), + quoteName(priv_key_path, "'"), + quoteName(priv_key_password, "'") + )) + + module.exit_json(changed = True, name = name) + +def sqlcmd(login_port, login_name, login_password, command): + subprocess.check_call([ + '/opt/mssql-tools/bin/sqlcmd', + '-S', + "localhost,{0}".format(login_port), + '-U', + login_name, + '-P', + login_password, + '-b', + '-Q', + command + ]) + +def quoteName(name, quote_char): + if quote_char == '[' or quote_char == ']': + (quote_start_char, quote_end_char) = ('[', ']') + elif quote_char == "'": + (quote_start_char, quote_end_char) = ("N'", "'") + else: + raise Exception("Unsupported quote_char {0}, must be [ or ] or '".format(quote_char)) + + return "{0}{1}{2}".format(quote_start_char, name.replace(quote_end_char, quote_end_char + quote_end_char), quote_end_char) + +if __name__ == '__main__': + main() diff --git a/samples/features/high availability/Linux/Ansible Playbook/library/mssql_conf.py b/samples/features/high availability/Linux/Ansible Playbook/library/mssql_conf.py new file mode 100644 index 00000000..cacd9975 --- /dev/null +++ b/samples/features/high availability/Linux/Ansible Playbook/library/mssql_conf.py @@ -0,0 +1,138 @@ +#!/usr/bin/python + +# Copyright (c) 2017 Microsoft Corporation + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'supported_by': 'community', + 'status': ['preview'] +} + +DOCUMENTATION = ''' +--- +module: mssql_conf + +short_description: Set configuration settings for a SQL Server instance + +description: + - Set configuration settings for a SQL Server instance + +version_added: "2.2" + +author: Arnav Singh (@arsing) + +options: + setup_sa_password: + description: + - The password to set for the sa account in setup + required: false + + name: + description: + - The name of the setting + required: false + + value: + description: + - The value of the setting + required: false + + login_port: + description: + - The TDS port of the instance + required: false + default: 1433 + + + login_name: + description: + - The name of the user to log in to the instance + required: true + + login_password: + description: + - The password of the user to log in to the instance + required: true + +requirements: + - python >= 2.7 +'''.replace('\t', ' ') + +EXAMPLES = ''' +# Enables HADRON +- mssql_conf: + name: hadr.hadrenabled + value: 1 + login_name: sa + login_password: password +'''.replace('\t', ' ') + +RETURN = ''' +# +'''.replace('\t', ' ') + + +from ansible.module_utils.basic import AnsibleModule +import os.path +import subprocess + +def main(): + module = AnsibleModule( + argument_spec = dict( + setup_sa_password = dict(required = False, no_log = True), + setup_pid = dict(required = False), + name = dict(required = False), + value = dict(required = False), + traceflags_on = dict(type = 'list', required = False), + traceflags_off = dict(type = 'list', required = False), + login_port = dict(required = False, default = 1433), + login_name = dict(required = True), + login_password = dict(required = True, no_log = True) + ), + required_one_of = [ + ['setup_sa_password', 'name', 'traceflags_on', 'traceflags_off'] + ], + mutually_exclusive = [ + ['setup_sa_password', 'name', 'traceflags_on'], + ['setup_sa_password', 'name', 'traceflags_off'], + ], + required_together = [ + ['setup_sa_password', 'setup_pid'], + ['name', 'value'] + ], + ) + + setup_sa_password = module.params['setup_sa_password'] + setup_pid = module.params['setup_pid'] + name = module.params['name'] + value = module.params['value'] + traceflags_on = module.params['traceflags_on'] + traceflags_off = module.params['traceflags_off'] + login_port = module.params['login_port'] + login_name = module.params['login_name'] + login_password = module.params['login_password'] + + changed = True + + if setup_sa_password is not None: + if os.path.isfile('/var/opt/mssql/mssql.conf'): + changed = False + else: + setup_env = os.environ.copy() + subprocess.check_call( + ['/opt/mssql/bin/mssql-conf', '--noprompt', 'setup', 'accept-eula'], + env = { 'MSSQL_SA_PASSWORD': setup_sa_password, 'MSSQL_PID': setup_pid }) + + if name is not None: + subprocess.check_call(['/opt/mssql/bin/mssql-conf', '--noprompt', 'set', name, value]) + + if traceflags_on is not None: + subprocess.check_call(['/opt/mssql/bin/mssql-conf', '--noprompt', 'traceflag'] + [str(traceflag) for traceflag in traceflags_on] + ['on']) + + if traceflags_off is not None: + subprocess.check_call(['/opt/mssql/bin/mssql-conf', '--noprompt', 'traceflag'] + [str(traceflag) for traceflag in traceflags_off] + ['off']) + + module.exit_json(changed = changed) + +if __name__ == '__main__': + main() diff --git a/samples/features/high availability/Linux/Ansible Playbook/library/mssql_endpoint.py b/samples/features/high availability/Linux/Ansible Playbook/library/mssql_endpoint.py new file mode 100644 index 00000000..5ba5501e --- /dev/null +++ b/samples/features/high availability/Linux/Ansible Playbook/library/mssql_endpoint.py @@ -0,0 +1,201 @@ +#!/usr/bin/python + +# Copyright (c) 2017 Microsoft Corporation + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'supported_by': 'community', + 'status': ['preview'] +} + +DOCUMENTATION = ''' +--- +module: mssql_login + +short_description: Add endpoints to a SQL Server instance + +description: + - Add endpoints to a SQL Server instance. + +version_added: "2.2" + +author: Arnav Singh (@arsing) + +options: + name: + description: + - The name of the endpoint to add + required: true + + ip: + description: + - The IP to bind to + required: false + default: 0.0.0.0 + + port: + description: + - The port to bind to + required: true + + type: + description: + - The type of the endpoint + required: true + choices: ["DATA_MIRRORING"] + + dbm_cert_name: + description: + - The name of the cert to use for the DATA_MIRRORING endpoint + required: false + default: [] + + state: + description: + - The state to set the endpoint to + required: false + choices: + - started + default: started + + login_port: + description: + - The TDS port of the instance + required: false + default: 1433 + + login_name: + description: + - The name of the user to log in to the instance + required: true + + login_password: + description: + - The password of the user to log in to the instance + required: true + +notes: + - Requires the mssql-tools package on the remote host. + +requirements: + - python >= 2.7 + - mssql-tools +'''.replace('\t', ' ') + +EXAMPLES = ''' +# Create a DBM endpoint named 'foo' on port 5022 with authentication from the cert named 'bar' +- mssql_endpoint: + name: foo + port: 5022 + type: DATA_MIRRORING + dbm_cert_name: bar + state: started + login_name: sa + login_password: password +'''.replace('\t', ' ') + +RETURN = ''' +name: + description: The name of the endpoint that was added + returned: success + type: string + sample: foo +'''.replace('\t', ' ') + + +from ansible.module_utils.basic import AnsibleModule +import subprocess + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(required = True), + ip = dict(required = False, default = "0.0.0.0"), + port = dict(required = True), + type = dict(choices = ['DATA_MIRRORING'], required = True), + dbm_cert_name = dict(required = False), + state = dict(choices = ['started'], required = False, default = 'started'), + login_port = dict(required = False, default = 1433), + login_name = dict(required = True), + login_password = dict(required = True, no_log = True) + ), + required_if = [ + ['type', 'DATA_MIRRORING', ['dbm_cert_name']] + ] + ) + + name = module.params['name'] + ip = module.params['ip'] + port = module.params['port'] + type = module.params['type'] + dbm_cert_name = module.params['dbm_cert_name'] + state = module.params['state'] + login_port = module.params['login_port'] + login_name = module.params['login_name'] + login_password = module.params['login_password'] + + if type == "DATA_MIRRORING": + options = """ + ROLE = ALL, + AUTHENTICATION = CERTIFICATE {0}, + ENCRYPTION = REQUIRED ALGORITHM AES + """.format( + quoteName(dbm_cert_name, '[') + ) + + sqlcmd(login_port, login_name, login_password, """ + IF NOT EXISTS( + SELECT * FROM sys.tcp_endpoints WHERE name = {0} + ) + CREATE ENDPOINT {1} + AS TCP (LISTENER_IP = ({2}), LISTENER_PORT = {3}) + FOR DATA_MIRRORING ({4}) + ; + """.format( + quoteName(name, "'"), + quoteName(name, '['), + ip, + port, + options + )) + + if state == 'started': + sqlcmd(login_port, login_name, login_password, """ + IF NOT EXISTS( + SELECT * FROM sys.tcp_endpoints WHERE name = {0} AND state = 0 + ) + ALTER ENDPOINT {1} STATE = STARTED + ; + """.format( + quoteName(name, "'"), + quoteName(name, '[') + )) + + module.exit_json(changed = True, name = name) + +def sqlcmd(login_port, login_name, login_password, command): + subprocess.check_call([ + '/opt/mssql-tools/bin/sqlcmd', + '-S', + "localhost,{0}".format(login_port), + '-U', + login_name, + '-P', + login_password, + '-b', + '-Q', + command + ]) + +def quoteName(name, quote_char): + if quote_char == '[' or quote_char == ']': + (quote_start_char, quote_end_char) = ('[', ']') + elif quote_char == "'": + (quote_start_char, quote_end_char) = ("N'", "'") + else: + raise Exception("Unsupported quote_char {0}, must be [ or ] or '".format(quote_char)) + + return "{0}{1}{2}".format(quote_start_char, name.replace(quote_end_char, quote_end_char + quote_end_char), quote_end_char) + +if __name__ == '__main__': + main() diff --git a/samples/features/high availability/Linux/Ansible Playbook/library/mssql_grant_endpoint.py b/samples/features/high availability/Linux/Ansible Playbook/library/mssql_grant_endpoint.py new file mode 100644 index 00000000..5f3bd376 --- /dev/null +++ b/samples/features/high availability/Linux/Ansible Playbook/library/mssql_grant_endpoint.py @@ -0,0 +1,140 @@ +#!/usr/bin/python + +# Copyright (c) 2017 Microsoft Corporation + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'supported_by': 'community', + 'status': ['preview'] +} + +DOCUMENTATION = ''' +--- +module: mssql_grant_endpoint + +short_description: Grants permissions on endpoints of a SQL Server instance + +description: + - Grants permissions on endpoints of a SQL Server instance. + +version_added: "2.2" + +author: Arnav Singh (@arsing) + +options: + name: + description: + - The name of the endpoint + required: true + + permission: + description: + - The permission to grant on the endpoint + required: true + + principal: + description: + - The principal to grant the permission to + required: true + + login_port: + description: + - The TDS port of the instance + required: false + default: 1433 + + login_name: + description: + - The name of the user to log in to the instance + required: true + + login_password: + description: + - The password of the user to log in to the instance + required: true + +notes: + - Requires the mssql-tools package on the remote host. + +requirements: + - python >= 2.7 + - mssql-tools +'''.replace('\t', ' ') + +EXAMPLES = ''' +# Grants CONNECT permission on the DBM endpoint named 'foo' to the login 'bar' +- mssql_endpoint: + name: foo + permission: CONNECT + principal: bar + login_name: sa + login_password: password +'''.replace('\t', ' ') + +RETURN = ''' +name: + description: The name of the login that was added + returned: success + type: string + sample: foo +'''.replace('\t', ' ') + + +from ansible.module_utils.basic import AnsibleModule +import subprocess + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(required = True), + permission = dict(choices = ["CONNECT"], required = True), + principal = dict(required = True), + login_port = dict(required = False, default = 1433), + login_name = dict(required = True), + login_password = dict(required = True, no_log = True) + ) + ) + + permission = module.params['permission'] + name = module.params['name'] + principal = module.params['principal'] + login_port = module.params['login_port'] + login_name = module.params['login_name'] + login_password = module.params['login_password'] + + sqlcmd(login_port, login_name, login_password, """ + GRANT {0} ON ENDPOINT::{1} TO {2} + """.format( + permission, + quoteName(name, '['), + quoteName(principal, '[') + )) + + module.exit_json(changed = True, name = name) + +def sqlcmd(login_port, login_name, login_password, command): + subprocess.check_call([ + '/opt/mssql-tools/bin/sqlcmd', + '-S', + "localhost,{0}".format(login_port), + '-U', + login_name, + '-P', + login_password, + '-b', + '-Q', + command + ]) + +def quoteName(name, quote_char): + if quote_char == '[' or quote_char == ']': + (quote_start_char, quote_end_char) = ('[', ']') + elif quote_char == "'": + (quote_start_char, quote_end_char) = ("N'", "'") + else: + raise Exception("Unsupported quote_char {0}, must be [ or ] or '".format(quote_char)) + + return "{0}{1}{2}".format(quote_start_char, name.replace(quote_end_char, quote_end_char + quote_end_char), quote_end_char) + +if __name__ == '__main__': + main() diff --git a/samples/features/high availability/Linux/Ansible Playbook/library/mssql_login.py b/samples/features/high availability/Linux/Ansible Playbook/library/mssql_login.py new file mode 100644 index 00000000..366dc9a5 --- /dev/null +++ b/samples/features/high availability/Linux/Ansible Playbook/library/mssql_login.py @@ -0,0 +1,160 @@ +#!/usr/bin/python + +# Copyright (c) 2017 Microsoft Corporation + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'supported_by': 'community', + 'status': ['preview'] +} + +DOCUMENTATION = ''' +--- +module: mssql_login + +short_description: Add logins to a SQL Server instance + +description: + - Add logins to a SQL Server instance. + +version_added: "2.2" + +author: Arnav Singh (@arsing) + +options: + name: + description: + - The name of the login to add + required: true + + password: + description: + - The password of the login + required: true + + roles: + description: + - The roles to add the login to + required: false + default: [] + + login_port: + description: + - The TDS port of the instance + required: false + default: 1433 + + login_name: + description: + - The name of the user to log in to the instance + required: true + + login_password: + description: + - The password of the user to log in to the instance + required: true + +notes: + - Requires the mssql-tools package on the remote host. + +requirements: + - python >= 2.7 + - mssql-tools +'''.replace('\t', ' ') + +EXAMPLES = ''' +# Create a login named 'foo' with password 'bar' and add it to sysadmin role +- mssql_login: + name: foo + password: bar + roles: + - sysadmin + login_name: sa + login_password: password +'''.replace('\t', ' ') + +RETURN = ''' +name: + description: The name of the login that was added + returned: success + type: string + sample: foo +'''.replace('\t', ' ') + + +from ansible.module_utils.basic import AnsibleModule +import subprocess + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(required = True), + password = dict(required = True, no_log = True), + roles = dict(type = 'list', required = False, default = []), + login_port = dict(required = False, default = 1433), + login_name = dict(required = True), + login_password = dict(required = True, no_log = True) + ) + ) + + name = module.params['name'] + password = module.params['password'] + roles = module.params['roles'] + login_port = module.params['login_port'] + login_name = module.params['login_name'] + login_password = module.params['login_password'] + + sqlcmd(login_port, login_name, login_password, """ + IF EXISTS( + SELECT * FROM sys.sql_logins WHERE name = {0} + ) + ALTER LOGIN {1} WITH PASSWORD = {2} + ELSE + CREATE LOGIN {1} WITH + PASSWORD = {2}, + DEFAULT_DATABASE = [master], + CHECK_EXPIRATION = OFF, + CHECK_POLICY = OFF + ; + """.format( + quoteName(name, "'"), + quoteName(name, '['), + quoteName(password, "'") + )) + + for role in roles: + sqlcmd(login_port, login_name, login_password, """ + ALTER SERVER ROLE {0} ADD MEMBER {1} + """.format( + quoteName(role, '['), + quoteName(name, '[') + )) + + module.exit_json(changed = True, name = name) + +def sqlcmd(login_port, login_name, login_password, command): + subprocess.check_call([ + '/opt/mssql-tools/bin/sqlcmd', + '-S', + "localhost,{0}".format(login_port), + '-U', + login_name, + '-P', + login_password, + '-b', + '-Q', + command + ]) + +def quoteName(name, quote_char): + if quote_char == '[' or quote_char == ']': + (quote_start_char, quote_end_char) = ('[', ']') + elif quote_char == "'": + (quote_start_char, quote_end_char) = ("N'", "'") + else: + raise Exception("Unsupported quote_char {0}, must be [ or ] or '".format(quote_char)) + + return "{0}{1}{2}".format(quote_start_char, name.replace(quote_end_char, quote_end_char + quote_end_char), quote_end_char) + +if __name__ == '__main__': + main() diff --git a/samples/features/high availability/Linux/Ansible Playbook/library/mssql_master_key.py b/samples/features/high availability/Linux/Ansible Playbook/library/mssql_master_key.py new file mode 100644 index 00000000..8de55a05 --- /dev/null +++ b/samples/features/high availability/Linux/Ansible Playbook/library/mssql_master_key.py @@ -0,0 +1,123 @@ +#!/usr/bin/python + +# Copyright (c) 2017 Microsoft Corporation + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'supported_by': 'community', + 'status': ['preview'] +} + +DOCUMENTATION = ''' +--- +module: mssql_master_key + +short_description: Add master keys to a SQL Server instance + +description: + - Add master keys to a SQL Server instance. + +version_added: "2.2" + +author: Arnav Singh (@arsing) + +options: + password: + description: + - The password of the master key + required: true + + login_port: + description: + - The TDS port of the instance + required: false + default: 1433 + + login_name: + description: + - The name of the user to log in to the instance + required: true + + login_password: + description: + - The password of the user to log in to the instance + required: true + +notes: + - Requires the mssql-tools package on the remote host. + +requirements: + - python >= 2.7 + - mssql-tools +'''.replace('\t', ' ') + +EXAMPLES = ''' +# Create a master key with password 'foo' +- mssql_master_key: + password: foo + login_name: sa + login_password: password +'''.replace('\t', ' ') + +RETURN = ''' +# +'''.replace('\t', ' ') + + +from ansible.module_utils.basic import AnsibleModule +import subprocess + +def main(): + module = AnsibleModule( + argument_spec = dict( + password = dict(required = True, no_log = True), + login_port = dict(required = False, default = 1433), + login_name = dict(required = True), + login_password = dict(required = True, no_log = True) + ) + ) + + password = module.params['password'] + login_port = module.params['login_port'] + login_name = module.params['login_name'] + login_password = module.params['login_password'] + + sqlcmd(login_port, login_name, login_password, """ + IF EXISTS ( + SELECT * FROM sys.symmetric_keys WHERE name = '##MS_DatabaseMasterKey##' + ) + ALTER MASTER KEY REGENERATE WITH ENCRYPTION BY PASSWORD = {0} + ELSE + CREATE MASTER KEY ENCRYPTION BY PASSWORD = {0} + """.format( + quoteName(password, "'") + )) + + module.exit_json(changed = True) + +def sqlcmd(login_port, login_name, login_password, command): + subprocess.check_call([ + '/opt/mssql-tools/bin/sqlcmd', + '-S', + "localhost,{0}".format(login_port), + '-U', + login_name, + '-P', + login_password, + '-b', + '-Q', + command + ]) + +def quoteName(name, quote_char): + if quote_char == '[' or quote_char == ']': + (quote_start_char, quote_end_char) = ('[', ']') + elif quote_char == "'": + (quote_start_char, quote_end_char) = ("N'", "'") + else: + raise Exception("Unsupported quote_char {0}, must be [ or ] or '".format(quote_char)) + + return "{0}{1}{2}".format(quote_start_char, name.replace(quote_end_char, quote_end_char + quote_end_char), quote_end_char) + +if __name__ == '__main__': + main() diff --git a/samples/features/high availability/Linux/Ansible Playbook/library/mssql_user.py b/samples/features/high availability/Linux/Ansible Playbook/library/mssql_user.py new file mode 100644 index 00000000..c316b242 --- /dev/null +++ b/samples/features/high availability/Linux/Ansible Playbook/library/mssql_user.py @@ -0,0 +1,136 @@ +#!/usr/bin/python + +# Copyright (c) 2017 Microsoft Corporation + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'supported_by': 'community', + 'status': ['preview'] +} + +DOCUMENTATION = ''' +--- +module: mssql_user + +short_description: Add users to a SQL Server instance + +description: + - Add ysers to a SQL Server instance. + +version_added: "2.2" + +author: Arnav Singh (@arsing) + +options: + name: + description: + - The name of the user to add + required: true + + login: + description: + - The login name of the user to add + required: true + + login_port: + description: + - The TDS port of the instance + required: false + default: 1433 + + login_name: + description: + - The name of the user to log in to the instance + required: true + + login_password: + description: + - The password of the user to log in to the instance + required: true + +notes: + - Requires the mssql-tools package on the remote host. + +requirements: + - python >= 2.7 + - mssql-tools +'''.replace('\t', ' ') + +EXAMPLES = ''' +# Create a user named 'foo' for the login named 'bar' +- mssql_user: + name: foo + login: bar + login_name: sa + login_password: password +'''.replace('\t', ' ') + +RETURN = ''' +name: + description: The name of the user that was added + returned: success + type: string + sample: foo +'''.replace('\t', ' ') + + +from ansible.module_utils.basic import AnsibleModule +import subprocess + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(required = True), + login = dict(required = True), + login_port = dict(required = False, default = 1433), + login_name = dict(required = True), + login_password = dict(required = True, no_log = True) + ) + ) + + name = module.params['name'] + login = module.params['login'] + login_port = module.params['login_port'] + login_name = module.params['login_name'] + login_password = module.params['login_password'] + + sqlcmd(login_port, login_name, login_password, """ + IF NOT EXISTS( + SELECT * FROM sys.sysusers WHERE name = {0} + ) + CREATE USER {1} FOR LOGIN {2} + ; + """.format( + quoteName(name, "'"), + quoteName(name, '['), + quoteName(login, '[') + )) + + module.exit_json(changed = True, name = name) + +def sqlcmd(login_port, login_name, login_password, command): + subprocess.check_call([ + '/opt/mssql-tools/bin/sqlcmd', + '-S', + "localhost,{0}".format(login_port), + '-U', + login_name, + '-P', + login_password, + '-b', + '-Q', + command + ]) + +def quoteName(name, quote_char): + if quote_char == '[' or quote_char == ']': + (quote_start_char, quote_end_char) = ('[', ']') + elif quote_char == "'": + (quote_start_char, quote_end_char) = ("N'", "'") + else: + raise Exception("Unsupported quote_char {0}, must be [ or ] or '".format(quote_char)) + + return "{0}{1}{2}".format(quote_start_char, name.replace(quote_end_char, quote_end_char + quote_end_char), quote_end_char) + +if __name__ == '__main__': + main() diff --git a/samples/features/high availability/Linux/Ansible Playbook/play.yml b/samples/features/high availability/Linux/Ansible Playbook/play.yml new file mode 100644 index 00000000..d3a594eb --- /dev/null +++ b/samples/features/high availability/Linux/Ansible Playbook/play.yml @@ -0,0 +1,75 @@ +--- + +# Usage: +# ansible-playbook ./play.yml -i ./inventory --ask-vault-pass -e 'ansible_user=<>' + +- hosts: servers + + vars: + # The URL of the repo to fetch the mssql-server and mssql-server-ha packages from + centos_server_repo_url: 'https://packages.microsoft.com/config/rhel/7/mssql-server.repo' + ubuntu_server_repo_url: 'https://packages.microsoft.com/config/ubuntu/16.04/mssql-server.list' + + # The URL of the repo to fetch the mssql-tools package from + centos_tools_repo_url: 'https://packages.microsoft.com/config/rhel/7/prod.repo' + ubuntu_tools_repo_url: 'https://packages.microsoft.com/config/ubuntu/16.04/prod.list' + + # The sqlservr PID. Only used if mssql-server needs to be installed. + pid: 'Developer' + + # The port for the TSQL endpoint + tsql_endpoint_port: 1433 + + # The name of the DBM endpoint + dbm_endpoint_name: 'dbm_endpoint' + + # The port for the DBM endpoint + dbm_endpoint_port: 5022 + + # The SQL login and SQL username for the DBM endpoint user + dbm_login: 'dbm_login' + dbm_username: 'dbm_user' + + # The name of the DBM cert + dbm_cert_name: 'dbm_cert' + + # The path of the DBM cert public key accessible *from the machine running this playbook*. + # The path need not be accessible from the nodes that are being deployed to. + dbm_cert_pub: /share/dbm_certificate.cer + + # The path of the DBM cert private key accessible *from the machine running this playbook*. + # The path need not be accessible from the nodes that are being deployed to. + dbm_cert_priv: /share/dbm_certificate.pvk + + # The filename to store the DBM cert public key as (/var/opt/mssql/data/) + dbm_cert_pub_target: dbm_certificate.cer + + # The filename to store the DBM cert private key as (/var/opt/mssql/data/) + dbm_cert_priv_target: dbm_certificate.pvk + + # The name of the AG and AG pacemaker resource + ag_name: 'ag1' + + # The IP of the AG listener + ag_listener_ip: '' + + # The NIC that the AG listener IP should be set on + ag_listener_nic: 'eno1' + + # The name of the pacemaker cluster + pacemaker_cluster_name: 'mycluster' + + # The SQL login for the pacemaker user + pacemaker_login: 'pacemaker' + + vars_files: + - 'vault.yml' + + become: yes + become_user: root + become_method: sudo + any_errors_fatal: true + max_fail_percentage: 0 + + roles: + - mssql-server-ag-external diff --git a/samples/features/high availability/Linux/Ansible Playbook/roles/mssql-server-ag-external/meta/main.yml b/samples/features/high availability/Linux/Ansible Playbook/roles/mssql-server-ag-external/meta/main.yml new file mode 100644 index 00000000..d34d60bc --- /dev/null +++ b/samples/features/high availability/Linux/Ansible Playbook/roles/mssql-server-ag-external/meta/main.yml @@ -0,0 +1,5 @@ +--- + +dependencies: +- role: pacemaker +- role: mssql-server-ha diff --git a/samples/features/high availability/Linux/Ansible Playbook/roles/mssql-server-ag-external/tasks/main.yml b/samples/features/high availability/Linux/Ansible Playbook/roles/mssql-server-ag-external/tasks/main.yml new file mode 100644 index 00000000..484ec17a --- /dev/null +++ b/samples/features/high availability/Linux/Ansible Playbook/roles/mssql-server-ag-external/tasks/main.yml @@ -0,0 +1,94 @@ +--- + +# install pacemaker resource agents + +- name: install mssql-server-ha package + package: + name: mssql-server-ha + state: latest + + +# create AG + +- name: create AG with all replicas in secondary role + mssql_ag: + name: "{{ ag_name }}" + state: all_secondaries_or_unjoined + login_port: "{{ tsql_endpoint_port }}" + login_name: 'sa' + login_password: "{{ sa_password }}" + +- name: promote one replica to AG primary and join others to it + mssql_ag: + name: "{{ ag_name }}" + state: all_joined_to_one_primary + all_replicas: "{{ groups['servers'] }}" + primary: "{{ groups['servers'][0] }}" + local_replica: "{{ inventory_hostname }}" + dbm_endpoint_port: "{{ dbm_endpoint_port }}" + login_port: "{{ tsql_endpoint_port }}" + login_name: 'sa' + login_password: "{{ sa_password }}" + + +# create listener + +- name: create listener + mssql_ag_listener: + name: "{{ ag_name }}_listener" + ag_name: "{{ ag_name }}" + ip: + - "{{ ag_listener_ip }}" + readonly_routing_replicas: "{{ groups['servers'] }}" + login_port: "{{ tsql_endpoint_port }}" + login_name: 'sa' + login_password: "{{ sa_password }}" + when: inventory_hostname == groups['servers'][0] and ag_listener_ip != '' + + +# create pacemaker login + +- name: create pacemaker login + mssql_login: + name: "{{ pacemaker_login }}" + password: "{{ pacemaker_password }}" + roles: + - sysadmin + login_port: "{{ tsql_endpoint_port }}" + login_name: 'sa' + login_password: "{{ sa_password }}" + +- name: create pacemaker passwd file + copy: + content: | + {{ pacemaker_login }} + {{ pacemaker_password }} + dest: /var/opt/mssql/secrets/passwd + mode: 0400 + + +# create AG pacemaker resource + +- name: create AG pacemaker resource + shell: | + pcs resource show '{{ ag_name }}' || + pcs resource create '{{ ag_name }}' ocf:mssql:ag \ + 'ag_name={{ ag_name }}' \ + --master meta \ + master-max=1 master-node-max=1 clone-max={{ groups['servers']|length }} notify=true + when: inventory_hostname == groups['servers'][0] + + +# Create AG listener pacemaker resource + +- name: create AG listener pacemaker resource + shell: | + pcs resource show '{{ ag_name }}_listener' || \ + pcs resource create '{{ ag_name }}_listener' ocf:heartbeat:IPaddr2 \ + 'ip={{ ag_listener_ip }}' \ + 'nic={{ ag_listener_nic }}' + when: inventory_hostname == groups['servers'][0] + +- name: colocate AG listener with AG primary + command: pcs constraint colocation add '{{ ag_name }}_listener' with master '{{ ag_name }}'-master + when: inventory_hostname == groups['servers'][0] diff --git a/samples/features/high availability/Linux/Ansible Playbook/roles/mssql-server-ha/meta/main.yml b/samples/features/high availability/Linux/Ansible Playbook/roles/mssql-server-ha/meta/main.yml new file mode 100644 index 00000000..d5bcab03 --- /dev/null +++ b/samples/features/high availability/Linux/Ansible Playbook/roles/mssql-server-ha/meta/main.yml @@ -0,0 +1,4 @@ +--- + +dependencies: +- role: mssql-server diff --git a/samples/features/high availability/Linux/Ansible Playbook/roles/mssql-server-ha/tasks/main.yml b/samples/features/high availability/Linux/Ansible Playbook/roles/mssql-server-ha/tasks/main.yml new file mode 100644 index 00000000..7ae04b63 --- /dev/null +++ b/samples/features/high availability/Linux/Ansible Playbook/roles/mssql-server-ha/tasks/main.yml @@ -0,0 +1,118 @@ +--- + +# enable HA + +- name: check HA enabled + command: /opt/mssql-tools/bin/sqlcmd -U sa -P '{{ sa_password }}' -b -Q "SELECT 'IsHadrEnabled = ' + CONVERT(NVARCHAR(100), SERVERPROPERTY('IsHadrEnabled'))" + register: hadr_enabled + +- name: enable HA + mssql_conf: + name: hadr.hadrenabled + value: 1 + login_name: 'sa' + login_password: "{{ sa_password }}" + when: hadr_enabled.stdout.find('IsHadrEnabled = 1') == -1 + +- name: restart sqlservr to enable HA + service: + name: mssql-server + state: restarted + when: hadr_enabled.stdout.find('IsHadrEnabled = 1') == -1 + + +# DBM endpoint + +- name: check if firewalld is installed (CentOS, RedHat) + command: rpm -q firewalld + register: firewalld_installed + failed_when: false + when: ansible_distribution in ['CentOS', 'RedHat'] + +- name: open DBM endpoint in firewall (CentOS, RedHat) + firewalld: + port: "{{ dbm_endpoint_port }}/tcp" + state: enabled + permanent: true + when: (ansible_distribution in ['CentOS', 'RedHat']) and firewalld_installed.rc == 0 + +- name: reload firewall (CentOS, RedHat) + command: firewall-cmd --reload + when: (ansible_distribution in ['CentOS', 'RedHat']) and firewalld_installed.rc == 0 + +- name: open DBM endpoint in firewall (Ubuntu) + ufw: + port: "{{ dbm_endpoint_port }}" + proto: tcp + rule: allow + when: ansible_distribution == 'Ubuntu' + +- name: copy DBM cert public key + copy: + src: "{{ dbm_cert_pub }}" + dest: "/var/opt/mssql/data/{{ dbm_cert_pub_target }}" + owner: mssql + group: mssql + mode: 0444 + +- name: copy DBM cert private key + copy: + src: "{{ dbm_cert_priv }}" + dest: "/var/opt/mssql/data/{{ dbm_cert_priv_target }}" + owner: mssql + group: mssql + mode: 0400 + +- name: create master key + mssql_master_key: + password: "{{ master_key_password }}" + login_port: "{{ tsql_endpoint_port }}" + login_name: 'sa' + login_password: "{{ sa_password }}" + +- name: create DBM endpoint login + mssql_login: + name: "{{ dbm_login }}" + password: "{{ dbm_password }}" + login_port: "{{ tsql_endpoint_port }}" + login_name: 'sa' + login_password: "{{ sa_password }}" + +- name: create DBM endpoint user + mssql_user: + name: "{{ dbm_username }}" + login: "{{ dbm_login }}" + login_port: "{{ tsql_endpoint_port }}" + login_name: 'sa' + login_password: "{{ sa_password }}" + +- name: create DBM endpoint certificate + mssql_certificate: + name: "{{ dbm_cert_name }}" + authorization_username: "{{ dbm_username }}" + pub_key_path: "/var/opt/mssql/data/{{ dbm_cert_pub_target }}" + priv_key_path: "/var/opt/mssql/data/{{ dbm_cert_priv_target }}" + priv_key_password: "{{ dbm_cert_password }}" + login_port: "{{ tsql_endpoint_port }}" + login_name: 'sa' + login_password: "{{ sa_password }}" + +- name: create DBM endpoint + mssql_endpoint: + name: "{{ dbm_endpoint_name }}" + port: "{{ dbm_endpoint_port }}" + type: DATA_MIRRORING + dbm_cert_name: "{{ dbm_cert_name }}" + state: started + login_port: "{{ tsql_endpoint_port }}" + login_name: 'sa' + login_password: "{{ sa_password }}" + +- name: grant connect on DBM endpoint + mssql_grant_endpoint: + name: "{{ dbm_endpoint_name }}" + permission: CONNECT + principal: "{{ dbm_login }}" + login_port: "{{ tsql_endpoint_port }}" + login_name: 'sa' + login_password: "{{ sa_password }}" diff --git a/samples/features/high availability/Linux/Ansible Playbook/roles/mssql-server/tasks/main.yml b/samples/features/high availability/Linux/Ansible Playbook/roles/mssql-server/tasks/main.yml new file mode 100644 index 00000000..c7449ba6 --- /dev/null +++ b/samples/features/high availability/Linux/Ansible Playbook/roles/mssql-server/tasks/main.yml @@ -0,0 +1,102 @@ +--- + +# install mssql-server package + +- name: install mssql-server repo (CentOS, RedHat) + get_url: + url: "{{ centos_server_repo_url }}" + dest: /etc/yum.repos.d/mssql-server.repo + when: ansible_distribution in ['CentOS', 'RedHat'] + +- name: install mssql-server repo (Ubuntu) + get_url: + url: "{{ ubuntu_server_repo_url }}" + dest: /etc/apt/sources.list.d/mssql-server.list + when: ansible_distribution == 'Ubuntu' + +- name: refresh apt-get cache for server repo (Ubuntu) + command: apt-get update + when: ansible_distribution == 'Ubuntu' + +- name: install mssql-server package + package: + name: mssql-server + state: latest + + +# setup + +- name: mssql-server setup + mssql_conf: + setup_sa_password: "{{ sa_password }}" + setup_pid: "{{ pid }}" + login_name: 'sa' + login_password: "{{ sa_password }}" + + +# TSQL endpoint + +- name: set TSQL endpoint port + mssql_conf: + name: network.tcpport + value: "{{ tsql_endpoint_port }}" + login_name: 'sa' + login_password: "{{ sa_password }}" + +- name: check if firewalld is installed (CentOS, RedHat) + command: rpm -q firewalld + register: firewalld_installed + failed_when: false + when: ansible_distribution in ['CentOS', 'RedHat'] + +- name: open TSQL endpoint in firewall (CentOS, RedHat) + firewalld: + port: "{{ tsql_endpoint_port }}/tcp" + state: enabled + permanent: true + when: (ansible_distribution in ['CentOS', 'RedHat']) and firewalld_installed.rc == 0 + +- name: reload firewall (CentOS, RedHat) + command: firewall-cmd --reload + when: (ansible_distribution in ['CentOS', 'RedHat']) and firewalld_installed.rc == 0 + +- name: open TSQL endpoint in firewall (Ubuntu) + ufw: + port: "{{ tsql_endpoint_port }}" + proto: tcp + rule: allow + when: ansible_distribution == 'Ubuntu' + + +# mssql-tools package + +- name: install mssql-tools repo (CentOS, RedHat) + get_url: + url: "{{ centos_tools_repo_url }}" + dest: /etc/yum.repos.d/mssql-tools.repo + when: ansible_distribution in ['CentOS', 'RedHat'] + +- name: install mssql-tools repo (Ubuntu) + get_url: + url: "{{ ubuntu_tools_repo_url }}" + dest: /etc/apt/sources.list.d/mssql-tools.list + when: ansible_distribution == 'Ubuntu' + +- name: refresh apt-get cache for tools repo (Ubuntu) + command: apt-get update + when: ansible_distribution == 'Ubuntu' + +- name: install mssql-tools package + package: + name: mssql-tools + state: latest + environment: + ACCEPT_EULA: 'y' + + +# Start mssql-server service + +- name: start sqlservr + service: + name: mssql-server + state: started diff --git a/samples/features/high availability/Linux/Ansible Playbook/roles/pacemaker/tasks/main.yml b/samples/features/high availability/Linux/Ansible Playbook/roles/pacemaker/tasks/main.yml new file mode 100644 index 00000000..aad7f542 --- /dev/null +++ b/samples/features/high availability/Linux/Ansible Playbook/roles/pacemaker/tasks/main.yml @@ -0,0 +1,96 @@ +--- + +# enable HA repo + +- name: Enable HA repo (RedHat) + command: subscription-manager repos --enable=rhel-ha-for-rhel-7-server-rpms + when: ansible_distribution == 'RedHat' + + +# install pacemaker + +- name: ensure pacemaker is installed (CentOS, RedHat) + package: + name: + - fence-agents-all + - pacemaker + - pcs + - resource-agents + state: latest + when: ansible_distribution in ['CentOS', 'RedHat'] + +- name: ensure pacemaker is installed (Ubuntu) + package: + name: + - fence-agents + - pacemaker + - pcs + - resource-agents + state: latest + when: ansible_distribution == 'Ubuntu' + +- name: check if firewalld is installed (CentOS, RedHat) + command: rpm -q firewalld + register: firewalld_installed + failed_when: false + when: ansible_distribution in ['CentOS', 'RedHat'] + +- name: open pacemaker in firewall (CentOS, RedHat) + firewalld: + service: high-availability + state: enabled + permanent: true + immediate: true + when: (ansible_distribution in ['CentOS', 'RedHat']) and firewalld_installed.rc == 0 + +- name: reload firewall (CentOS, RedHat) + command: firewall-cmd --reload + when: (ansible_distribution in ['CentOS', 'RedHat']) and firewalld_installed.rc == 0 + +- name: open pacemaker in firewall (TCP) (Ubuntu) + ufw: + port: "{{ item }}" + proto: tcp + rule: allow + with_items: + - 2224 + - 3121 + - 21064 + when: ansible_distribution == 'Ubuntu' + +- name: open pacemaker in firewall (UDP) (Ubuntu) + ufw: + port: 5405 + proto: udp + rule: allow + when: ansible_distribution == 'Ubuntu' + +- name: setup pacemaker admin user + user: + name: hacluster + password: "{{ pacemaker_cluster_password|password_hash('sha512') }}" + +- name: setup pcsd service + service: + name: pcsd + state: started + enabled: yes + +- name: setup pacemaker service + service: + name: pacemaker + enabled: yes + + +# create pacemaker cluster + +- name: create pacemaker cluster + shell: | + pcs cluster auth -u hacluster -p '{{ pacemaker_cluster_password }}' {% for server in groups['servers'] %} {{ server }} {% endfor %} && + pcs cluster setup --name '{{ pacemaker_cluster_name }}' {% for server in groups['servers'] %} {{ server }} {% endfor %} && + pcs cluster start --all && + pcs property set stonith-enabled=false && + pcs property set start-failure-is-fatal=false + args: + creates: /etc/corosync/corosync.conf + when: inventory_hostname == groups['servers'][0]