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