]> git.proxmox.com Git - ceph.git/blob - ceph/qa/tasks/rgw_multisite.py
a41238daa0419792384bb85681f8a04548ee5dee
[ceph.git] / ceph / qa / tasks / rgw_multisite.py
1 """
2 rgw multisite configuration routines
3 """
4 import argparse
5 import contextlib
6 import logging
7 import random
8 import string
9 from copy import deepcopy
10 from util.rgw import rgwadmin, wait_for_radosgw
11 from util.rados import create_ec_pool, create_replicated_pool
12 from rgw_multi import multisite
13 from rgw_multi.zone_rados import RadosZone as RadosZone
14
15 from teuthology.orchestra import run
16 from teuthology import misc
17 from teuthology.exceptions import ConfigError
18 from teuthology.task import Task
19
20 log = logging.getLogger(__name__)
21
22 class RGWMultisite(Task):
23 """
24 Performs rgw multisite configuration to match the given realm definition.
25
26 - rgw-multisite:
27 realm:
28 name: test-realm
29 is_default: true
30
31 List one or more zonegroup definitions. These are provided as json
32 input to `radosgw-admin zonegroup set`, with the exception of these keys:
33
34 * 'is_master' is passed on the command line as --master
35 * 'is_default' is passed on the command line as --default
36 * 'endpoints' given as client names are replaced with actual endpoints
37
38 zonegroups:
39 - name: test-zonegroup
40 api_name: test-api
41 is_master: true
42 is_default: true
43 endpoints: [c1.client.0]
44
45 List each of the zones to be created in this zonegroup.
46
47 zones:
48 - name: test-zone1
49 is_master: true
50 is_default: true
51 endpoints: [c1.client.0]
52 - name: test-zone2
53 is_default: true
54 endpoints: [c2.client.0]
55
56 A complete example:
57
58 tasks:
59 - install:
60 - ceph: {cluster: c1}
61 - ceph: {cluster: c2}
62 - rgw:
63 c1.client.0:
64 c2.client.0:
65 - rgw-multisite:
66 realm:
67 name: test-realm
68 is_default: true
69 zonegroups:
70 - name: test-zonegroup
71 is_master: true
72 is_default: true
73 zones:
74 - name: test-zone1
75 is_master: true
76 is_default: true
77 endpoints: [c1.client.0]
78 - name: test-zone2
79 is_default: true
80 endpoints: [c2.client.0]
81
82 """
83 def __init__(self, ctx, config):
84 super(RGWMultisite, self).__init__(ctx, config)
85
86 def setup(self):
87 super(RGWMultisite, self).setup()
88
89 overrides = self.ctx.config.get('overrides', {})
90 misc.deep_merge(self.config, overrides.get('rgw-multisite', {}))
91
92 if not self.ctx.rgw:
93 raise ConfigError('rgw-multisite must run after the rgw task')
94 role_endpoints = self.ctx.rgw.role_endpoints
95
96 # construct Clusters and Gateways for each client in the rgw task
97 clusters, gateways = extract_clusters_and_gateways(self.ctx,
98 role_endpoints)
99
100 # get the master zone and zonegroup configuration
101 mz, mzg = extract_master_zone_zonegroup(self.config['zonegroups'])
102 cluster1 = cluster_for_zone(clusters, mz)
103
104 # create the realm and period on the master zone's cluster
105 log.info('creating realm..')
106 realm = create_realm(cluster1, self.config['realm'])
107 period = realm.current_period
108
109 creds = gen_credentials()
110
111 # create the master zonegroup and its master zone
112 log.info('creating master zonegroup..')
113 master_zonegroup = create_zonegroup(cluster1, gateways, period,
114 deepcopy(mzg))
115 period.master_zonegroup = master_zonegroup
116
117 log.info('creating master zone..')
118 master_zone = create_zone(self.ctx, cluster1, gateways, creds,
119 master_zonegroup, deepcopy(mz))
120 master_zonegroup.master_zone = master_zone
121
122 period.update(master_zone, commit=True)
123 restart_zone_gateways(master_zone) # restart with --rgw-zone
124
125 # create the admin user on the master zone
126 log.info('creating admin user..')
127 user_args = ['--display-name', 'Realm Admin', '--system']
128 user_args += creds.credential_args()
129 admin_user = multisite.User('realm-admin')
130 admin_user.create(master_zone, user_args)
131
132 # process 'zonegroups'
133 for zg_config in self.config['zonegroups']:
134 zones_config = zg_config.pop('zones')
135
136 zonegroup = None
137 for zone_config in zones_config:
138 # get the cluster for this zone
139 cluster = cluster_for_zone(clusters, zone_config)
140
141 if cluster != cluster1: # already created on master cluster
142 log.info('pulling realm configuration to %s', cluster.name)
143 realm.pull(cluster, master_zone.gateways[0], creds)
144
145 # use the first zone's cluster to create the zonegroup
146 if not zonegroup:
147 if zg_config['name'] == master_zonegroup.name:
148 zonegroup = master_zonegroup
149 else:
150 log.info('creating zonegroup..')
151 zonegroup = create_zonegroup(cluster, gateways,
152 period, zg_config)
153
154 if zone_config['name'] == master_zone.name:
155 # master zone was already created
156 zone = master_zone
157 else:
158 # create the zone and commit the period
159 log.info('creating zone..')
160 zone = create_zone(self.ctx, cluster, gateways, creds,
161 zonegroup, zone_config)
162 period.update(zone, commit=True)
163
164 restart_zone_gateways(zone) # restart with --rgw-zone
165
166 # attach configuration to the ctx for other tasks
167 self.ctx.rgw_multisite = argparse.Namespace()
168 self.ctx.rgw_multisite.clusters = clusters
169 self.ctx.rgw_multisite.gateways = gateways
170 self.ctx.rgw_multisite.realm = realm
171 self.ctx.rgw_multisite.admin_user = admin_user
172
173 log.info('rgw multisite configuration completed')
174
175 def end(self):
176 del self.ctx.rgw_multisite
177
178 class Cluster(multisite.Cluster):
179 """ Issues 'radosgw-admin' commands with the rgwadmin() helper """
180 def __init__(self, ctx, name, client):
181 super(Cluster, self).__init__()
182 self.ctx = ctx
183 self.name = name
184 self.client = client
185
186 def admin(self, args = None, **kwargs):
187 """ radosgw-admin command """
188 args = args or []
189 args += ['--cluster', self.name]
190 args += ['--debug-rgw', str(kwargs.pop('debug_rgw', 0))]
191 args += ['--debug-ms', str(kwargs.pop('debug_ms', 0))]
192 if kwargs.pop('read_only', False):
193 args += ['--rgw-cache-enabled', 'false']
194 kwargs['decode'] = False
195 check_retcode = kwargs.pop('check_retcode', True)
196 r, s = rgwadmin(self.ctx, self.client, args, **kwargs)
197 if check_retcode:
198 assert r == 0
199 return s, r
200
201 class Gateway(multisite.Gateway):
202 """ Controls a radosgw instance using its daemon """
203 def __init__(self, role, remote, daemon, *args, **kwargs):
204 super(Gateway, self).__init__(*args, **kwargs)
205 self.role = role
206 self.remote = remote
207 self.daemon = daemon
208
209 def set_zone(self, zone):
210 """ set the zone and add its args to the daemon's command line """
211 assert self.zone is None, 'zone can only be set once'
212 self.zone = zone
213 # daemon.restart_with_args() would be perfect for this, except that
214 # radosgw args likely include a pipe and redirect. zone arguments at
215 # the end won't actually apply to radosgw
216 args = self.daemon.command_kwargs.get('args', [])
217 try:
218 # insert zone args before the first |
219 pipe = args.index(run.Raw('|'))
220 args = args[0:pipe] + zone.zone_args() + args[pipe:]
221 except ValueError, e:
222 args += zone.zone_args()
223 self.daemon.command_kwargs['args'] = args
224
225 def start(self, args = None):
226 """ (re)start the daemon """
227 self.daemon.restart()
228 # wait until startup completes
229 wait_for_radosgw(self.endpoint(), self.remote)
230
231 def stop(self):
232 """ stop the daemon """
233 self.daemon.stop()
234
235 def extract_clusters_and_gateways(ctx, role_endpoints):
236 """ create cluster and gateway instances for all of the radosgw roles """
237 clusters = {}
238 gateways = {}
239 for role, endpoint in role_endpoints.iteritems():
240 cluster_name, daemon_type, client_id = misc.split_role(role)
241 # find or create the cluster by name
242 cluster = clusters.get(cluster_name)
243 if not cluster:
244 clusters[cluster_name] = cluster = Cluster(ctx, cluster_name, role)
245 # create a gateway for this daemon
246 client_with_id = daemon_type + '.' + client_id # match format from rgw.py
247 daemon = ctx.daemons.get_daemon('rgw', client_with_id, cluster_name)
248 if not daemon:
249 raise ConfigError('no daemon for role=%s cluster=%s type=rgw id=%s' % \
250 (role, cluster_name, client_id))
251 (remote,) = ctx.cluster.only(role).remotes.keys()
252 gateways[role] = Gateway(role, remote, daemon, endpoint.hostname,
253 endpoint.port, cluster)
254 return clusters, gateways
255
256 def create_realm(cluster, config):
257 """ create a realm from configuration and initialize its first period """
258 realm = multisite.Realm(config['name'])
259 args = []
260 if config.get('is_default', False):
261 args += ['--default']
262 realm.create(cluster, args)
263 realm.current_period = multisite.Period(realm)
264 return realm
265
266 def extract_user_credentials(config):
267 """ extract keys from configuration """
268 return multisite.Credentials(config['access_key'], config['secret_key'])
269
270 def extract_master_zone(zonegroup_config):
271 """ find and return the master zone definition """
272 master = None
273 for zone in zonegroup_config['zones']:
274 if not zone.get('is_master', False):
275 continue
276 if master:
277 raise ConfigError('zones %s and %s cannot both set \'is_master\'' % \
278 (master['name'], zone['name']))
279 master = zone
280 # continue the loop so we can detect duplicates
281 if not master:
282 raise ConfigError('one zone must set \'is_master\' in zonegroup %s' % \
283 zonegroup_config['name'])
284 return master
285
286 def extract_master_zone_zonegroup(zonegroups_config):
287 """ find and return the master zone and zonegroup definitions """
288 master_zone, master_zonegroup = (None, None)
289 for zonegroup in zonegroups_config:
290 # verify that all zonegroups have a master zone set, even if they
291 # aren't in the master zonegroup
292 zone = extract_master_zone(zonegroup)
293 if not zonegroup.get('is_master', False):
294 continue
295 if master_zonegroup:
296 raise ConfigError('zonegroups %s and %s cannot both set \'is_master\'' % \
297 (master_zonegroup['name'], zonegroup['name']))
298 master_zonegroup = zonegroup
299 master_zone = zone
300 # continue the loop so we can detect duplicates
301 if not master_zonegroup:
302 raise ConfigError('one zonegroup must set \'is_master\'')
303 return master_zone, master_zonegroup
304
305 def extract_zone_cluster_name(zone_config):
306 """ return the cluster (must be common to all zone endpoints) """
307 cluster_name = None
308 endpoints = zone_config.get('endpoints')
309 if not endpoints:
310 raise ConfigError('zone %s missing \'endpoints\' list' % \
311 zone_config['name'])
312 for role in endpoints:
313 name, _, _ = misc.split_role(role)
314 if not cluster_name:
315 cluster_name = name
316 elif cluster_name != name:
317 raise ConfigError('all zone %s endpoints must be in the same cluster' % \
318 zone_config['name'])
319 return cluster_name
320
321 def cluster_for_zone(clusters, zone_config):
322 """ return the cluster entry for the given zone """
323 name = extract_zone_cluster_name(zone_config)
324 try:
325 return clusters[name]
326 except KeyError:
327 raise ConfigError('no cluster %s found' % name)
328
329 def gen_access_key():
330 return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(16))
331
332 def gen_secret():
333 return ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(32))
334
335 def gen_credentials():
336 return multisite.Credentials(gen_access_key(), gen_secret())
337
338 def extract_gateway_endpoints(gateways, endpoints_config):
339 """ return a list of gateway endpoints associated with the given roles """
340 endpoints = []
341 for role in endpoints_config:
342 try:
343 # replace role names with their gateway's endpoint
344 endpoints.append(gateways[role].endpoint())
345 except KeyError:
346 raise ConfigError('no radosgw endpoint found for role %s' % role)
347 return endpoints
348
349 def is_default_arg(config):
350 return ['--default'] if config.pop('is_default', False) else []
351
352 def is_master_arg(config):
353 return ['--master'] if config.pop('is_master', False) else []
354
355 def create_zonegroup(cluster, gateways, period, config):
356 """ pass the zonegroup configuration to `zonegroup set` """
357 config.pop('zones', None) # remove 'zones' from input to `zonegroup set`
358 endpoints = config.get('endpoints')
359 if endpoints:
360 # replace client names with their gateway endpoints
361 config['endpoints'] = extract_gateway_endpoints(gateways, endpoints)
362 zonegroup = multisite.ZoneGroup(config['name'], period)
363 # `zonegroup set` needs --default on command line, and 'is_master' in json
364 args = is_default_arg(config)
365 zonegroup.set(cluster, config, args)
366 period.zonegroups.append(zonegroup)
367 return zonegroup
368
369 def create_zone(ctx, cluster, gateways, creds, zonegroup, config):
370 """ create a zone with the given configuration """
371 zone = multisite.Zone(config['name'], zonegroup, cluster)
372 zone = RadosZone(config['name'], zonegroup, cluster)
373
374 # collect Gateways for the zone's endpoints
375 endpoints = config.get('endpoints')
376 if not endpoints:
377 raise ConfigError('no \'endpoints\' for zone %s' % config['name'])
378 zone.gateways = [gateways[role] for role in endpoints]
379 for gateway in zone.gateways:
380 gateway.set_zone(zone)
381
382 # format the gateway endpoints
383 endpoints = [g.endpoint() for g in zone.gateways]
384
385 args = is_default_arg(config)
386 args += is_master_arg(config)
387 args += creds.credential_args()
388 if len(endpoints):
389 args += ['--endpoints', ','.join(endpoints)]
390 zone.create(cluster, args)
391 zonegroup.zones.append(zone)
392
393 create_zone_pools(ctx, zone)
394 if ctx.rgw.compression_type:
395 configure_zone_compression(zone, ctx.rgw.compression_type)
396
397 zonegroup.zones_by_type.setdefault(zone.tier_type(), []).append(zone)
398
399 if zone.is_read_only():
400 zonegroup.ro_zones.append(zone)
401 else:
402 zonegroup.rw_zones.append(zone)
403
404 return zone
405
406 def create_zone_pools(ctx, zone):
407 """ Create the data_pool for each placement type """
408 gateway = zone.gateways[0]
409 cluster = zone.cluster
410 for pool_config in zone.data.get('placement_pools', []):
411 pool_name = pool_config['val']['storage_classes']['STANDARD']['data_pool']
412 if ctx.rgw.ec_data_pool:
413 create_ec_pool(gateway.remote, pool_name, zone.name, 64,
414 ctx.rgw.erasure_code_profile, cluster.name, 'rgw')
415 else:
416 create_replicated_pool(gateway.remote, pool_name, 64, cluster.name, 'rgw')
417
418 def configure_zone_compression(zone, compression):
419 """ Set compression type in the zone's default-placement """
420 zone.json_command(zone.cluster, 'placement', ['modify',
421 '--placement-id', 'default-placement',
422 '--compression', compression
423 ])
424
425 def restart_zone_gateways(zone):
426 zone.stop()
427 zone.start()
428
429 task = RGWMultisite