]>
Commit | Line | Data |
---|---|---|
3efd9988 FG |
1 | from __future__ import print_function |
2 | import argparse | |
b32b8144 | 3 | import base64 |
a8e16298 | 4 | import glob |
3efd9988 FG |
5 | import json |
6 | import logging | |
7 | import os | |
8 | from textwrap import dedent | |
b32b8144 | 9 | from ceph_volume import process, decorators, terminal, conf |
3efd9988 | 10 | from ceph_volume.util import system, disk |
b32b8144 | 11 | from ceph_volume.util import encryption as encryption_utils |
3efd9988 FG |
12 | from ceph_volume.systemd import systemctl |
13 | ||
14 | ||
15 | logger = logging.getLogger(__name__) | |
b32b8144 | 16 | mlogger = terminal.MultiLogger(__name__) |
3efd9988 FG |
17 | |
18 | ||
19 | class Activate(object): | |
20 | ||
21 | help = 'Enable systemd units to mount configured devices and start a Ceph OSD' | |
22 | ||
91327a77 | 23 | def __init__(self, argv, from_trigger=False): |
3efd9988 | 24 | self.argv = argv |
91327a77 AA |
25 | self.from_trigger = from_trigger |
26 | self.skip_systemd = False | |
3efd9988 | 27 | |
b32b8144 FG |
28 | def validate_devices(self, json_config): |
29 | """ | |
30 | ``json_config`` is the loaded dictionary coming from the JSON file. It is usually mixed with | |
31 | other non-device items, but for sakes of comparison it doesn't really matter. This method is | |
32 | just making sure that the keys needed exist | |
33 | """ | |
34 | devices = json_config.keys() | |
35 | try: | |
36 | objectstore = json_config['type'] | |
37 | except KeyError: | |
05a536ef TL |
38 | logger.warning( |
39 | '"type" key not found, assuming "bluestore" since journal key is not present' | |
40 | ) | |
41 | objectstore = 'bluestore' | |
b32b8144 FG |
42 | |
43 | # Go through all the device combinations that are absolutely required, | |
44 | # raise an error describing what was expected and what was found | |
45 | # otherwise. | |
05a536ef | 46 | if objectstore == 'bluestore': |
b32b8144 FG |
47 | # This is a bit tricky, with newer bluestore we don't need data, older implementations |
48 | # do (e.g. with ceph-disk). ceph-volume just uses a tmpfs that doesn't require data. | |
49 | if {'block', 'data'}.issubset(set(devices)): | |
50 | return True | |
51 | else: | |
52 | bluestore_devices = ['block.db', 'block.wal', 'block', 'data'] | |
53 | found = [i for i in devices if i in bluestore_devices] | |
54 | mlogger.error("Required devices (block and data) not present for bluestore") | |
55 | mlogger.error('bluestore devices found: %s', found) | |
56 | raise RuntimeError('Unable to activate bluestore OSD due to missing devices') | |
57 | ||
58 | def get_device(self, uuid): | |
59 | """ | |
60 | If a device is encrypted, it will decrypt/open and return the mapper | |
61 | path, if it isn't encrypted it will just return the device found that | |
62 | is mapped to the uuid. This will make it easier for the caller to | |
63 | avoid if/else to check if devices need decrypting | |
64 | ||
65 | :param uuid: The partition uuid of the device (PARTUUID) | |
66 | """ | |
67 | device = disk.get_device_from_partuuid(uuid) | |
68 | ||
69 | # If device is not found, it is fine to return an empty string from the | |
70 | # helper that finds `device`. If it finds anything and it is not | |
71 | # encrypted, just return what was found | |
72 | if not self.is_encrypted or not device: | |
73 | return device | |
74 | ||
75 | if self.encryption_type == 'luks': | |
76 | encryption_utils.luks_open(self.dmcrypt_secret, device, uuid) | |
77 | else: | |
78 | encryption_utils.plain_open(self.dmcrypt_secret, device, uuid) | |
79 | ||
80 | return '/dev/mapper/%s' % uuid | |
81 | ||
91327a77 AA |
82 | def enable_systemd_units(self, osd_id, osd_fsid): |
83 | """ | |
84 | * disables the ceph-disk systemd units to prevent them from running when | |
85 | a UDEV event matches Ceph rules | |
86 | * creates the ``simple`` systemd units to handle the activation and | |
87 | startup of the OSD with ``osd_id`` and ``osd_fsid`` | |
88 | * enables the OSD systemd unit and finally starts the OSD. | |
89 | """ | |
90 | if not self.from_trigger and not self.skip_systemd: | |
91 | # means it was scanned and now activated directly, so ensure that | |
92 | # ceph-disk units are disabled, and that the `simple` systemd unit | |
93 | # is created and enabled | |
94 | ||
95 | # enable the ceph-volume unit for this OSD | |
96 | systemctl.enable_volume(osd_id, osd_fsid, 'simple') | |
97 | ||
98 | # disable any/all ceph-disk units | |
99 | systemctl.mask_ceph_disk() | |
100 | terminal.warning( | |
101 | ('All ceph-disk systemd units have been disabled to ' | |
102 | 'prevent OSDs getting triggered by UDEV events') | |
103 | ) | |
104 | else: | |
105 | terminal.info('Skipping enabling of `simple` systemd unit') | |
106 | terminal.info('Skipping masking of ceph-disk systemd units') | |
107 | ||
108 | if not self.skip_systemd: | |
109 | # enable the OSD | |
110 | systemctl.enable_osd(osd_id) | |
111 | ||
112 | # start the OSD | |
113 | systemctl.start_osd(osd_id) | |
114 | else: | |
115 | terminal.info( | |
116 | 'Skipping enabling and starting OSD simple systemd unit because --no-systemd was used' | |
117 | ) | |
118 | ||
3efd9988 FG |
119 | @decorators.needs_root |
120 | def activate(self, args): | |
121 | with open(args.json_config, 'r') as fp: | |
122 | osd_metadata = json.load(fp) | |
123 | ||
b32b8144 FG |
124 | # Make sure that required devices are configured |
125 | self.validate_devices(osd_metadata) | |
126 | ||
3efd9988 FG |
127 | osd_id = osd_metadata.get('whoami', args.osd_id) |
128 | osd_fsid = osd_metadata.get('fsid', args.osd_fsid) | |
3efd9988 | 129 | data_uuid = osd_metadata.get('data', {}).get('uuid') |
b32b8144 | 130 | conf.cluster = osd_metadata.get('cluster_name', 'ceph') |
3efd9988 FG |
131 | if not data_uuid: |
132 | raise RuntimeError( | |
133 | 'Unable to activate OSD %s - no "uuid" key found for data' % args.osd_id | |
134 | ) | |
b32b8144 FG |
135 | |
136 | # Encryption detection, and capturing of the keys to decrypt | |
137 | self.is_encrypted = osd_metadata.get('encrypted', False) | |
138 | self.encryption_type = osd_metadata.get('encryption_type') | |
139 | if self.is_encrypted: | |
140 | lockbox_secret = osd_metadata.get('lockbox.keyring') | |
141 | # write the keyring always so that we can unlock | |
142 | encryption_utils.write_lockbox_keyring(osd_id, osd_fsid, lockbox_secret) | |
143 | # Store the secret around so that the decrypt method can reuse | |
144 | raw_dmcrypt_secret = encryption_utils.get_dmcrypt_key(osd_id, osd_fsid) | |
145 | # Note how both these calls need b64decode. For some reason, the | |
146 | # way ceph-disk creates these keys, it stores them in the monitor | |
147 | # *undecoded*, requiring this decode call again. The lvm side of | |
148 | # encryption doesn't need it, so we are assuming here that anything | |
149 | # that `simple` scans, will come from ceph-disk and will need this | |
150 | # extra decode call here | |
151 | self.dmcrypt_secret = base64.b64decode(raw_dmcrypt_secret) | |
152 | ||
153 | cluster_name = osd_metadata.get('cluster_name', 'ceph') | |
154 | osd_dir = '/var/lib/ceph/osd/%s-%s' % (cluster_name, osd_id) | |
155 | ||
156 | # XXX there is no support for LVM here | |
157 | data_device = self.get_device(data_uuid) | |
f91f0fd5 TL |
158 | |
159 | if not data_device: | |
160 | raise RuntimeError("osd fsid {} doesn't exist, this file will " | |
161 | "be skipped, consider cleaning legacy " | |
162 | "json file {}".format(osd_metadata['fsid'], args.json_config)) | |
163 | ||
b32b8144 FG |
164 | block_device = self.get_device(osd_metadata.get('block', {}).get('uuid')) |
165 | block_db_device = self.get_device(osd_metadata.get('block.db', {}).get('uuid')) | |
166 | block_wal_device = self.get_device(osd_metadata.get('block.wal', {}).get('uuid')) | |
3efd9988 FG |
167 | |
168 | if not system.device_is_mounted(data_device, destination=osd_dir): | |
05a536ef | 169 | process.run(['mount', '-v', data_device, osd_dir]) |
3efd9988 FG |
170 | |
171 | device_map = { | |
3efd9988 FG |
172 | 'block': block_device, |
173 | 'block.db': block_db_device, | |
174 | 'block.wal': block_wal_device | |
175 | } | |
176 | ||
177 | for name, device in device_map.items(): | |
178 | if not device: | |
179 | continue | |
180 | # always re-do the symlink regardless if it exists, so that the journal | |
181 | # device path that may have changed can be mapped correctly every time | |
182 | destination = os.path.join(osd_dir, name) | |
b32b8144 | 183 | process.run(['ln', '-snf', device, destination]) |
3efd9988 FG |
184 | |
185 | # make sure that the journal has proper permissions | |
186 | system.chown(device) | |
187 | ||
91327a77 | 188 | self.enable_systemd_units(osd_id, osd_fsid) |
3efd9988 | 189 | |
b32b8144 | 190 | terminal.success('Successfully activated OSD %s with FSID %s' % (osd_id, osd_fsid)) |
3efd9988 FG |
191 | |
192 | def main(self): | |
193 | sub_command_help = dedent(""" | |
194 | Activate OSDs by mounting devices previously configured to their | |
195 | appropriate destination:: | |
196 | ||
197 | ceph-volume simple activate {ID} {FSID} | |
198 | ||
199 | Or using a JSON file directly:: | |
200 | ||
201 | ceph-volume simple activate --file /etc/ceph/osd/{ID}-{FSID}.json | |
202 | ||
203 | The OSD must have been "scanned" previously (see ``ceph-volume simple | |
204 | scan``), so that all needed OSD device information and metadata exist. | |
205 | ||
206 | A previously scanned OSD would exist like:: | |
207 | ||
208 | /etc/ceph/osd/{ID}-{FSID}.json | |
209 | ||
210 | ||
211 | Environment variables supported: | |
212 | ||
213 | CEPH_VOLUME_SIMPLE_JSON_DIR: Directory location for scanned OSD JSON configs | |
214 | """) | |
215 | parser = argparse.ArgumentParser( | |
216 | prog='ceph-volume simple activate', | |
217 | formatter_class=argparse.RawDescriptionHelpFormatter, | |
218 | description=sub_command_help, | |
219 | ) | |
220 | parser.add_argument( | |
221 | 'osd_id', | |
222 | metavar='ID', | |
223 | nargs='?', | |
224 | help='The ID of the OSD, usually an integer, like 0' | |
225 | ) | |
226 | parser.add_argument( | |
227 | 'osd_fsid', | |
228 | metavar='FSID', | |
229 | nargs='?', | |
230 | help='The FSID of the OSD, similar to a SHA1' | |
231 | ) | |
a8e16298 TL |
232 | parser.add_argument( |
233 | '--all', | |
234 | help='Activate all OSDs with a OSD JSON config', | |
235 | action='store_true', | |
236 | default=False, | |
237 | ) | |
3efd9988 FG |
238 | parser.add_argument( |
239 | '--file', | |
240 | help='The path to a JSON file, from a scanned OSD' | |
241 | ) | |
91327a77 AA |
242 | parser.add_argument( |
243 | '--no-systemd', | |
244 | dest='skip_systemd', | |
245 | action='store_true', | |
246 | help='Skip creating and enabling systemd units and starting OSD services', | |
247 | ) | |
3efd9988 FG |
248 | if len(self.argv) == 0: |
249 | print(sub_command_help) | |
250 | return | |
251 | args = parser.parse_args(self.argv) | |
a8e16298 | 252 | if not args.file and not args.all: |
3efd9988 FG |
253 | if not args.osd_id and not args.osd_fsid: |
254 | terminal.error('ID and FSID are required to find the right OSD to activate') | |
255 | terminal.error('from a scanned OSD location in /etc/ceph/osd/') | |
256 | raise RuntimeError('Unable to activate without both ID and FSID') | |
257 | # don't allow a CLI flag to specify the JSON dir, because that might | |
258 | # implicitly indicate that it would be possible to activate a json file | |
259 | # at a non-default location which would not work at boot time if the | |
260 | # custom location is not exposed through an ENV var | |
a8e16298 | 261 | self.skip_systemd = args.skip_systemd |
3efd9988 | 262 | json_dir = os.environ.get('CEPH_VOLUME_SIMPLE_JSON_DIR', '/etc/ceph/osd/') |
a8e16298 TL |
263 | if args.all: |
264 | if args.file or args.osd_id: | |
265 | mlogger.warn('--all was passed, ignoring --file and ID/FSID arguments') | |
266 | json_configs = glob.glob('{}/*.json'.format(json_dir)) | |
267 | for json_config in json_configs: | |
268 | mlogger.info('activating OSD specified in {}'.format(json_config)) | |
269 | args.json_config = json_config | |
f91f0fd5 TL |
270 | try: |
271 | self.activate(args) | |
272 | except RuntimeError as e: | |
273 | terminal.warning(e.message) | |
3efd9988 | 274 | else: |
a8e16298 TL |
275 | if args.file: |
276 | json_config = args.file | |
277 | else: | |
278 | json_config = os.path.join(json_dir, '%s-%s.json' % (args.osd_id, args.osd_fsid)) | |
279 | if not os.path.exists(json_config): | |
280 | raise RuntimeError('Expected JSON config path not found: %s' % json_config) | |
281 | args.json_config = json_config | |
282 | self.activate(args) |