Skip to content

Commit bcb5cb4

Browse files
committed
Added sample Ansible playbook for installing SQL Server and creating a Pacemaker-managed AG.
1 parent 18c3048 commit bcb5cb4

File tree

18 files changed

+2059
-0
lines changed

18 files changed

+2059
-0
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
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.
2+
3+
4+
# Roles
5+
6+
- `pacemaker` - This role creates a Pacemaker cluster between the hosts.
7+
- `mssql-server` - This role installs SQL Server on the host, runs setup to set the SA password, and starts the service.
8+
- `mssql-server-ha` - This role enables support for HA and creates a DB Mirroring endpoint.
9+
- `mssql-server-ag-external` - This role installs the Pacemaker resource agents, creates an AG, an optional listener, and Pacemaker resources for both.
10+
11+
12+
# Try
13+
14+
1. Put the names of the Linux nodes in the `inventory` file
15+
16+
1. Configure the deployment in `play.yml`
17+
18+
1. Create a vault file named `vault.yml` using the template at the end of this README.
19+
20+
```sh
21+
ansible-vault create vault.yml
22+
```
23+
24+
1. Execute the playbook
25+
26+
```sh
27+
ansible-playbook ./play.yml -i ./inventory --ask-vault-pass -e 'ansible_user=username'
28+
```
29+
30+
31+
# Vault file template
32+
33+
```yaml
34+
---
35+
36+
ansible_ssh_pass: 'some password'
37+
38+
ansible_sudo_pass: 'some password'
39+
40+
# The password for the sa user. Only used if mssql-server needs to be installed.
41+
sa_password: 'some password'
42+
43+
# The password for the master key
44+
master_key_password: 'some password'
45+
46+
# The SQL password for the DBM endpoint user
47+
dbm_password: 'some password'
48+
49+
# The password for the DBM cert private key
50+
dbm_cert_password: 'some password'
51+
52+
# The password of the user that admins the pacemaker cluster (hacluster)
53+
pacemaker_cluster_password: 'some password'
54+
55+
# The SQL password for the pacemaker user
56+
pacemaker_password: 'some password'
57+
```
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[servers]
2+
node1
3+
node2
4+
node3
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
#!/usr/bin/python
2+
3+
# Copyright (c) 2017 Microsoft Corporation
4+
5+
ANSIBLE_METADATA = {
6+
'metadata_version': '1.1',
7+
'supported_by': 'community',
8+
'status': ['preview']
9+
}
10+
11+
DOCUMENTATION = '''
12+
---
13+
module: mssql_ag
14+
15+
short_description: Add or join availability groups on a SQL Server instance
16+
17+
description:
18+
- Add or join availability groups on a SQL Server instance.
19+
20+
version_added: "2.2"
21+
22+
author: Arnav Singh (@arsing)
23+
24+
options:
25+
name:
26+
description:
27+
- The name of the availability group to add
28+
required: true
29+
30+
state:
31+
description:
32+
- The state to set the local replica to
33+
choices: ["all_secondaries_or_unjoined", "all_joined_to_one_primary"]
34+
required: true
35+
36+
all_replicas:
37+
description:
38+
- A list of all the replicas of the AG
39+
required: false
40+
41+
primary:
42+
description:
43+
- The replica that should become the primary
44+
required: false
45+
46+
local_replica:
47+
description:
48+
- The name of the local replica
49+
required: false
50+
51+
dbm_endpoint_port:
52+
description:
53+
- The port of the DBM endpoint
54+
required: false
55+
56+
login_port:
57+
description:
58+
- The TDS port of the instance
59+
required: false
60+
default: 1433
61+
62+
login_name:
63+
description:
64+
- The name of the user to log in to the instance
65+
required: true
66+
67+
login_password:
68+
description:
69+
- The password of the user to log in to the instance
70+
required: true
71+
72+
notes:
73+
- Requires the mssql-tools package on the remote host.
74+
75+
requirements:
76+
- python >= 2.7
77+
- mssql-tools
78+
'''.replace('\t', ' ')
79+
80+
EXAMPLES = '''
81+
# Set all replicas of AG foo to secondary
82+
- mssql_ag:
83+
name: foo
84+
state: all_secondaries_or_unjoined
85+
login_name: sa
86+
login_password: password
87+
88+
# Join all replicas of AG foo to primary on the first server in the group named servers
89+
- mssql_ag:
90+
name: foo
91+
state: all_joined_to_one_primary
92+
all_replicas: "{{ groups['servers'] }}"
93+
primary: "{{ groups['servers'][0] }}"
94+
local_replica: "{{ inventory_hostname }}"
95+
login_name: sa
96+
login_password: password
97+
'''.replace('\t', ' ')
98+
99+
RETURN = '''
100+
name:
101+
description: The name of the AG that was created or joined
102+
returned: success
103+
type: string
104+
sample: foo
105+
'''.replace('\t', ' ')
106+
107+
108+
from ansible.module_utils.basic import AnsibleModule
109+
import subprocess
110+
111+
def main():
112+
module = AnsibleModule(
113+
argument_spec = dict(
114+
name = dict(required = True),
115+
state = dict(choices = ['all_secondaries_or_unjoined', 'all_joined_to_one_primary'], required = True),
116+
all_replicas = dict(type = 'list', required = False),
117+
primary = dict(required = False),
118+
local_replica = dict(required = False),
119+
dbm_endpoint_port = dict(required = False),
120+
login_port = dict(required = False, default = 1433),
121+
login_name = dict(required = True),
122+
login_password = dict(required = True, no_log = True)
123+
),
124+
required_if = [
125+
['state', 'all_joined_to_one_primary', ['all_replicas', 'primary', 'local_replica', 'dbm_endpoint_port']]
126+
]
127+
)
128+
129+
name = module.params['name']
130+
state = module.params['state']
131+
all_replicas = module.params['all_replicas']
132+
primary = module.params['primary']
133+
local_replica = module.params['local_replica']
134+
dbm_endpoint_port = module.params['dbm_endpoint_port']
135+
login_port = module.params['login_port']
136+
login_name = module.params['login_name']
137+
login_password = module.params['login_password']
138+
139+
if state == "all_secondaries_or_unjoined":
140+
sqlcmd(login_port, login_name, login_password, """
141+
IF EXISTS (
142+
SELECT * FROM sys.availability_groups WHERE name = {0}
143+
)
144+
ALTER AVAILABILITY GROUP {1} SET (ROLE = SECONDARY)
145+
;
146+
""".format(
147+
quoteName(name, "'"),
148+
quoteName(name, '[')
149+
))
150+
151+
elif primary == local_replica:
152+
def replica_spec(name, endpoint_port):
153+
return """
154+
{0} WITH (
155+
ENDPOINT_URL = {1},
156+
AVAILABILITY_MODE = SYNCHRONOUS_COMMIT,
157+
FAILOVER_MODE = EXTERNAL,
158+
SEEDING_MODE = AUTOMATIC
159+
)
160+
""".format(
161+
quoteName(name.split('.')[0], "'"),
162+
quoteName('tcp://{0}:{1}'.format(name, endpoint_port), "'")
163+
)
164+
165+
sqlcmd(login_port, login_name, login_password, """
166+
IF NOT EXISTS (
167+
SELECT * FROM sys.availability_groups WHERE name = {0}
168+
)
169+
CREATE AVAILABILITY GROUP {1}
170+
WITH (CLUSTER_TYPE = EXTERNAL, DB_FAILOVER = ON)
171+
FOR REPLICA ON {2}
172+
ELSE IF NOT EXISTS (
173+
SELECT *
174+
FROM sys.dm_hadr_availability_replica_states ars
175+
JOIN sys.availability_groups ag ON ars.group_id = ag.group_id
176+
WHERE ag.name = {0} AND ars.is_local = 1 AND ars.role = 1
177+
)
178+
BEGIN
179+
EXEC sp_set_session_context @key = N'external_cluster', @value = N'yes', @read_only = 1
180+
ALTER AVAILABILITY GROUP {1} FAILOVER
181+
END
182+
;
183+
184+
ALTER AVAILABILITY GROUP {1} GRANT CREATE ANY DATABASE
185+
;
186+
""".format(
187+
quoteName(name, "'"),
188+
quoteName(name, '['),
189+
replica_spec(primary, dbm_endpoint_port)
190+
))
191+
192+
for replica in all_replicas:
193+
if replica != primary:
194+
sqlcmd(login_port, login_name, login_password, """
195+
IF NOT EXISTS (
196+
SELECT *
197+
FROM sys.availability_replicas ar
198+
JOIN sys.availability_groups ag ON ar.group_id = ag.group_id
199+
WHERE ag.name = {0} AND ar.replica_server_name = {2}
200+
)
201+
ALTER AVAILABILITY GROUP {1}
202+
ADD REPLICA ON {3}
203+
;
204+
""".format(
205+
quoteName(name, "'"),
206+
quoteName(name, '['),
207+
quoteName(replica.split('.')[0], "'"),
208+
replica_spec(replica, dbm_endpoint_port)
209+
))
210+
211+
else:
212+
sqlcmd(login_port, login_name, login_password, """
213+
IF NOT EXISTS (
214+
SELECT * FROM sys.availability_groups WHERE name = {0}
215+
)
216+
ALTER AVAILABILITY GROUP {1} JOIN WITH (CLUSTER_TYPE = EXTERNAL)
217+
;
218+
219+
ALTER AVAILABILITY GROUP {1} GRANT CREATE ANY DATABASE
220+
;
221+
""".format(
222+
quoteName(name, "'"),
223+
quoteName(name, '[')
224+
))
225+
226+
module.exit_json(changed = True, name = name)
227+
228+
def sqlcmd(login_port, login_name, login_password, command):
229+
subprocess.check_call([
230+
'/opt/mssql-tools/bin/sqlcmd',
231+
'-S',
232+
"localhost,{0}".format(login_port),
233+
'-U',
234+
login_name,
235+
'-P',
236+
login_password,
237+
'-b',
238+
'-Q',
239+
command
240+
])
241+
242+
def quoteName(name, quote_char):
243+
if quote_char == '[' or quote_char == ']':
244+
(quote_start_char, quote_end_char) = ('[', ']')
245+
elif quote_char == "'":
246+
(quote_start_char, quote_end_char) = ("N'", "'")
247+
else:
248+
raise Exception("Unsupported quote_char {0}, must be [ or ] or '".format(quote_char))
249+
250+
return "{0}{1}{2}".format(quote_start_char, name.replace(quote_end_char, quote_end_char + quote_end_char), quote_end_char)
251+
252+
if __name__ == '__main__':
253+
main()

0 commit comments

Comments
 (0)