]>
Commit | Line | Data |
---|---|---|
3efd9988 FG |
1 | from __future__ import print_function |
2 | import argparse | |
3 | import json | |
4 | import logging | |
5 | import os | |
6 | from textwrap import dedent | |
7 | from ceph_volume import decorators, terminal, conf | |
8 | from ceph_volume.api import lvm | |
9 | from ceph_volume.util import arg_validators, system, disk | |
10 | ||
11 | ||
12 | logger = logging.getLogger(__name__) | |
13 | ||
14 | ||
15 | class Scan(object): | |
16 | ||
17 | help = 'Capture metadata from an OSD data partition or directory' | |
18 | ||
19 | def __init__(self, argv): | |
20 | self.argv = argv | |
21 | self._etc_path = '/etc/ceph/osd/' | |
22 | ||
23 | @property | |
24 | def etc_path(self): | |
25 | if os.path.isdir(self._etc_path): | |
26 | return self._etc_path | |
27 | ||
28 | if not os.path.exists(self._etc_path): | |
29 | os.mkdir(self._etc_path) | |
30 | return self._etc_path | |
31 | ||
32 | error = "OSD Configuration path (%s) needs to be a directory" % self._etc_path | |
33 | raise RuntimeError(error) | |
34 | ||
35 | def get_contents(self, path): | |
36 | with open(path, 'r') as fp: | |
37 | contents = fp.readlines() | |
38 | if len(contents) > 1: | |
39 | return ''.join(contents) | |
40 | return ''.join(contents).strip().strip('\n') | |
41 | ||
42 | def scan_device(self, path): | |
43 | device_metadata = {'path': None, 'uuid': None} | |
44 | if not path: | |
45 | return device_metadata | |
46 | # cannot read the symlink if this is tmpfs | |
47 | if os.path.islink(path): | |
48 | device = os.readlink(path) | |
49 | else: | |
50 | device = path | |
51 | lvm_device = lvm.get_lv_from_argument(device) | |
52 | if lvm_device: | |
53 | device_uuid = lvm_device.lv_uuid | |
54 | else: | |
55 | device_uuid = disk.get_partuuid(device) | |
56 | ||
57 | device_metadata['uuid'] = device_uuid | |
58 | device_metadata['path'] = device | |
59 | ||
60 | return device_metadata | |
61 | ||
62 | def scan_directory(self, path): | |
63 | osd_metadata = {'cluster_name': conf.cluster} | |
64 | path_mounts = system.get_mounts(paths=True) | |
65 | for _file in os.listdir(path): | |
66 | file_path = os.path.join(path, _file) | |
67 | if os.path.islink(file_path): | |
68 | osd_metadata[_file] = self.scan_device(file_path) | |
69 | if os.path.isdir(file_path): | |
70 | continue | |
71 | # the check for binary needs to go before the file, to avoid | |
72 | # capturing data from binary files but still be able to capture | |
73 | # contents from actual files later | |
74 | if system.is_binary(file_path): | |
75 | continue | |
76 | if os.path.isfile(file_path): | |
77 | osd_metadata[_file] = self.get_contents(file_path) | |
78 | ||
79 | device = path_mounts.get(path) | |
80 | # it is possible to have more than one device, pick the first one, and | |
81 | # warn that it is possible that more than one device is 'data' | |
82 | if not device: | |
83 | terminal.error('Unable to detect device mounted for path: %s' % path) | |
84 | raise RuntimeError('Cannot activate OSD') | |
85 | osd_metadata['data'] = self.scan_device(device[0] if len(device) else None) | |
86 | ||
87 | return osd_metadata | |
88 | ||
89 | @decorators.needs_root | |
90 | def scan(self, args): | |
91 | osd_metadata = {'cluster_name': conf.cluster} | |
92 | device_mounts = system.get_mounts(devices=True) | |
93 | osd_path = None | |
94 | logger.info('detecting if argument is a device or a directory: %s', args.osd_path) | |
95 | if os.path.isdir(args.osd_path): | |
96 | logger.info('will scan directly, path is a directory') | |
97 | osd_path = args.osd_path | |
98 | else: | |
99 | # assume this is a device, check if it is mounted and use that path | |
100 | logger.info('path is not a directory, will check if mounted') | |
101 | if system.device_is_mounted(args.osd_path): | |
102 | logger.info('argument is a device, which is mounted') | |
103 | mounted_osd_paths = device_mounts.get(args.osd_path) | |
104 | osd_path = mounted_osd_paths[0] if len(mounted_osd_paths) else None | |
105 | ||
106 | # argument is not a directory, and it is not a device that is mounted | |
107 | # somewhere so temporarily mount it to poke inside, otherwise, scan | |
108 | # directly | |
109 | if not osd_path: | |
110 | logger.info('device is not mounted, will mount it temporarily to scan') | |
111 | with system.tmp_mount(args.osd_path) as osd_path: | |
112 | osd_metadata = self.scan_directory(osd_path) | |
113 | else: | |
114 | logger.info('will scan OSD directory at path: %s', osd_path) | |
115 | osd_metadata = self.scan_directory(osd_path) | |
116 | ||
117 | osd_id = osd_metadata['whoami'] | |
118 | osd_fsid = osd_metadata['fsid'] | |
119 | filename = '%s-%s.json' % (osd_id, osd_fsid) | |
120 | json_path = os.path.join(self.etc_path, filename) | |
121 | if os.path.exists(json_path) and not args.stdout: | |
122 | if not args.force: | |
123 | raise RuntimeError( | |
124 | '--force was not used and OSD metadata file exists: %s' % json_path | |
125 | ) | |
126 | ||
127 | if args.stdout: | |
128 | print(json.dumps(osd_metadata, indent=4, sort_keys=True, ensure_ascii=False)) | |
129 | else: | |
130 | with open(json_path, 'w') as fp: | |
131 | json.dump(osd_metadata, fp, indent=4, sort_keys=True, ensure_ascii=False) | |
132 | terminal.success( | |
133 | 'OSD %s got scanned and metadata persisted to file: %s' % ( | |
134 | osd_id, | |
135 | json_path | |
136 | ) | |
137 | ) | |
138 | terminal.success( | |
139 | 'To take over managment of this scanned OSD, and disable ceph-disk and udev, run:' | |
140 | ) | |
141 | terminal.success(' ceph-volume simple activate %s %s' % (osd_id, osd_fsid)) | |
142 | ||
143 | if not osd_metadata.get('data'): | |
144 | msg = 'Unable to determine device mounted on %s' % args.osd_path | |
145 | logger.warning(msg) | |
146 | terminal.warning(msg) | |
147 | terminal.warning('OSD will not be able to start without this information:') | |
148 | terminal.warning(' "data": "/path/to/device",') | |
149 | logger.warning('Unable to determine device mounted on %s' % args.osd_path) | |
150 | ||
151 | def main(self): | |
152 | sub_command_help = dedent(""" | |
153 | Scan an OSD directory for files and configurations that will allow to | |
154 | take over the management of the OSD. | |
155 | ||
156 | Scanned OSDs will get their configurations stored in | |
157 | /etc/ceph/osd/<id>-<fsid>.json | |
158 | ||
159 | For an OSD ID of 0 with fsid of ``a9d50838-e823-43d6-b01f-2f8d0a77afc2`` | |
160 | that could mean a scan command that looks like:: | |
161 | ||
162 | ceph-volume lvm scan /var/lib/ceph/osd/ceph-0 | |
163 | ||
164 | Which would store the metadata in a JSON file at:: | |
165 | ||
166 | /etc/ceph/osd/0-a9d50838-e823-43d6-b01f-2f8d0a77afc2.json | |
167 | ||
168 | To a scan an existing, running, OSD: | |
169 | ||
170 | ceph-volume simple scan /var/lib/ceph/osd/{cluster}-{osd id} | |
171 | ||
172 | And to scan a device (mounted or unmounted) that has OSD data in it, for example /dev/sda1 | |
173 | ||
174 | ceph-volume simple scan /dev/sda1 | |
175 | """) | |
176 | parser = argparse.ArgumentParser( | |
177 | prog='ceph-volume simple scan', | |
178 | formatter_class=argparse.RawDescriptionHelpFormatter, | |
179 | description=sub_command_help, | |
180 | ) | |
181 | ||
182 | parser.add_argument( | |
183 | '-f', '--force', | |
184 | action='store_true', | |
185 | help='If OSD has already been scanned, the JSON file will be overwritten' | |
186 | ) | |
187 | ||
188 | parser.add_argument( | |
189 | '--stdout', | |
190 | action='store_true', | |
191 | help='Do not save to a file, output metadata to stdout' | |
192 | ) | |
193 | ||
194 | parser.add_argument( | |
195 | 'osd_path', | |
196 | metavar='OSD_PATH', | |
197 | type=arg_validators.OSDPath(), | |
198 | nargs='?', | |
199 | help='Path to an existing OSD directory or OSD data partition' | |
200 | ) | |
201 | ||
202 | if len(self.argv) == 0: | |
203 | print(sub_command_help) | |
204 | return | |
205 | args = parser.parse_args(self.argv) | |
206 | self.scan(args) |