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