Skip to content

Commit 3431ac4

Browse files
authored
Merge pull request ceph#62038 from phlogistonjohn/jjm-cephadm-infer-69278
cephadm: update and improve container image infer function Reviewed-by: Adam King <[email protected]>
2 parents 5a91525 + ec5ef05 commit 3431ac4

File tree

6 files changed

+413
-86
lines changed

6 files changed

+413
-86
lines changed

src/cephadm/cephadm.py

Lines changed: 1 addition & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@
201201
MemUsageStatusUpdater,
202202
VersionStatusUpdater,
203203
)
204-
from cephadmlib.container_lookup import get_container_info
204+
from cephadmlib.container_lookup import infer_local_ceph_image
205205

206206

207207
FuncT = TypeVar('FuncT', bound=Callable)
@@ -465,51 +465,6 @@ def update_default_image(ctx: CephadmContext) -> None:
465465
ctx.image = _get_default_image(ctx)
466466

467467

468-
def infer_local_ceph_image(ctx: CephadmContext, container_path: str) -> Optional[str]:
469-
"""
470-
Infer the local ceph image based on the following priority criteria:
471-
1- the image specified by --image arg (if provided).
472-
2- the same image as the daemon container specified by --name arg (if provided).
473-
3- image used by any ceph container running on the host. In this case we use daemon types.
474-
4- if no container is found then we use the most ceph recent image on the host.
475-
476-
Note: any selected container must have the same fsid inferred previously.
477-
478-
:return: The most recent local ceph image (already pulled)
479-
"""
480-
# '|' special character is used to separate the output fields into:
481-
# - Repository@digest
482-
# - Image Id
483-
# - Image Tag
484-
# - Image creation date
485-
out, _, _ = call_throws(ctx,
486-
[container_path, 'images',
487-
'--filter', 'label=ceph=True',
488-
'--filter', 'dangling=false',
489-
'--format', '{{.Repository}}@{{.Digest}}|{{.ID}}|{{.Tag}}|{{.CreatedAt}}'])
490-
491-
container_info = None
492-
daemon_name = ctx.name if ('name' in ctx and ctx.name and '.' in ctx.name) else None
493-
daemons_ls = [daemon_name] if daemon_name is not None else ceph_daemons() # daemon types: 'mon', 'mgr', etc
494-
for daemon in daemons_ls:
495-
container_info = get_container_info(ctx, daemon, daemon_name is not None)
496-
if container_info is not None:
497-
logger.debug(f"Using container info for daemon '{daemon}'")
498-
break
499-
500-
for image in out.splitlines():
501-
if image and not image.isspace():
502-
(digest, image_id, tag, created_date) = image.lstrip().split('|')
503-
if container_info is not None and image_id not in container_info.image_id:
504-
continue
505-
if digest and not digest.endswith('@'):
506-
logger.info(f"Using ceph image with id '{image_id}' and tag '{tag}' created on {created_date}\n{digest}")
507-
return digest
508-
if container_info is not None:
509-
logger.warning(f"Not using image '{container_info.image_id}' as it's not in list of non-dangling images with ceph=True label")
510-
return None
511-
512-
513468
def get_log_dir(fsid, log_dir):
514469
# type: (str, str) -> str
515470
return os.path.join(log_dir, fsid)

src/cephadm/cephadmlib/container_engines.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,3 +430,111 @@ def normalize_container_id(i: str) -> str:
430430
if i.startswith(prefix):
431431
i = i[len(prefix) :]
432432
return i
433+
434+
435+
class ImageInfo:
436+
def __init__(
437+
self,
438+
image_id: str,
439+
repository: str,
440+
digest: str,
441+
tag: str,
442+
created: str,
443+
) -> None:
444+
self.image_id = image_id
445+
self.repository = repository
446+
self.digest = digest
447+
self.tag = tag
448+
self.created = created
449+
450+
@property
451+
def name_with_tag(self) -> str:
452+
if self.repository and self.tag:
453+
return f'{self.repository}:{self.tag}'
454+
return ''
455+
456+
@property
457+
def name_with_digest(self) -> str:
458+
if self.repository and self.digest:
459+
return f'{self.repository}@{self.digest}'
460+
return ''
461+
462+
@property
463+
def name(self) -> str:
464+
if self.name_with_digest:
465+
return self.name_with_digest
466+
if self.name_with_tag:
467+
return self.name_with_tag
468+
return self.image_id
469+
470+
def __repr__(self) -> str:
471+
return (
472+
'ImageInfo('
473+
f'image_id={self.image_id!r}, '
474+
f'repository={self.repository!r}, '
475+
f'digest={self.digest!r}, '
476+
f'tag={self.tag!r}, '
477+
f'created={self.created!r}'
478+
')'
479+
)
480+
481+
482+
def _container_image_list(
483+
ctx: CephadmContext,
484+
filters: Optional[List[str]] = None,
485+
*,
486+
container_path: str = '',
487+
) -> Tuple[str, str, int]:
488+
"""get images with stats from the container engine"""
489+
container_path = container_path or ctx.container_engine.path
490+
args = [container_path, 'images']
491+
for filter_value in filters or []:
492+
args.append(f'--filter={filter_value}')
493+
fmt = '{{.Repository}}@{{.Digest}}|{{.ID}}|{{.Tag}}|{{.CreatedAt}}'
494+
args.append(f'--format={fmt}')
495+
return call(ctx, args, verbosity=CallVerbosity.QUIET)
496+
497+
498+
def _parse_container_image_list(
499+
out: str,
500+
err: str,
501+
code: int,
502+
) -> List[ImageInfo]:
503+
if code != 0:
504+
return []
505+
images: List[ImageInfo] = []
506+
for line in out.splitlines():
507+
line = line.strip()
508+
if not line:
509+
continue
510+
try:
511+
digest_part, image_id, tag, created_date = line.split('|')
512+
except ValueError:
513+
raise ValueError('invalid container image value: {line!r}')
514+
try:
515+
repository, digest = (digest_part or '@').split('@')
516+
except ValueError:
517+
raise ValueError('invalid digest part: {digest_part!r}')
518+
tag = '' if tag == '<none>' else tag
519+
repository = '' if repository == '<none>' else repository
520+
image_info = ImageInfo(
521+
image_id=image_id,
522+
repository=repository,
523+
digest=digest,
524+
tag=tag,
525+
created=created_date,
526+
)
527+
images.append(image_info)
528+
return images
529+
530+
531+
def parsed_container_image_list(
532+
ctx: CephadmContext,
533+
filters: Optional[List[str]] = None,
534+
*,
535+
container_path: str = '',
536+
) -> List[ImageInfo]:
537+
out, err, code = _container_image_list(
538+
ctx, filters=filters, container_path=container_path
539+
)
540+
return _parse_container_image_list(out, err, code)

src/cephadm/cephadmlib/container_lookup.py

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
# container_lookup.py - high-level functions for getting container info
22

3-
from typing import Optional
3+
from operator import itemgetter
4+
from typing import Optional, Tuple
45

56
import logging
67

7-
from .container_engines import ContainerInfo, parsed_container_image_stats
8+
from .container_engines import (
9+
ContainerInfo,
10+
ImageInfo,
11+
parsed_container_image_list,
12+
parsed_container_image_stats,
13+
)
814
from .container_types import get_container_stats
915
from .context import CephadmContext
1016
from .daemon_identity import DaemonIdentity
17+
from .daemons.ceph import ceph_daemons
1118
from .listing import daemons_matching
1219
from .listing_updaters import CoreStatusUpdater
1320

@@ -89,3 +96,79 @@ def get_container_info(
8996
'bad daemon state: no image, not running: %r', matched_deamon
9097
)
9198
return None
99+
100+
101+
def infer_local_ceph_image(
102+
ctx: CephadmContext, container_path: str = ''
103+
) -> Optional[str]:
104+
"""Infer the best ceph image to use based on the following criteria:
105+
Out of all images labeled as ceph that are non-dangling, prefer
106+
1. the same image as the daemon container specified by -name arg (if provided).
107+
2. the image used by any ceph container running on the host
108+
3. the most ceph recent image on the host
109+
110+
:return: An image name or none
111+
"""
112+
# enumerate ceph images on the system
113+
images = parsed_container_image_list(
114+
ctx,
115+
filters=['dangling=false', 'label=ceph=True'],
116+
container_path=container_path,
117+
)
118+
if not images:
119+
logger.warning('No non-dangling ceph images found')
120+
return None # no images at all cached on host
121+
122+
# find running ceph daemons
123+
_daemons = ceph_daemons()
124+
daemon_name = getattr(ctx, 'name', '')
125+
_cinfo_key = '_container_info'
126+
_updater = CoreStatusUpdater(keep_container_info=_cinfo_key)
127+
matching_daemons = [
128+
itemgetter(_cinfo_key, 'name')(_updater.expand(ctx, entry))
129+
for entry in daemons_matching(
130+
ctx, fsid=ctx.fsid, daemon_type_predicate=lambda t: t in _daemons
131+
)
132+
]
133+
# collect the running ceph daemon image ids
134+
images_in_use_by_daemon = set(
135+
d.image_id for d, n in matching_daemons if n == daemon_name
136+
)
137+
images_in_use = set(d.image_id for d, _ in matching_daemons)
138+
139+
# prioritize images
140+
def _keyfunc(image: ImageInfo) -> Tuple[bool, bool, str]:
141+
return (
142+
bool(
143+
image.digest
144+
and any(
145+
v.startswith(image.image_id)
146+
for v in images_in_use_by_daemon
147+
)
148+
),
149+
bool(
150+
image.digest
151+
and any(v.startswith(image.image_id) for v in images_in_use)
152+
),
153+
image.created,
154+
)
155+
156+
images.sort(key=_keyfunc, reverse=True)
157+
best_image = images[0]
158+
name_match, ceph_match, _ = _keyfunc(best_image)
159+
reason = 'not in the list of non-dangling images with ceph=True label'
160+
if images_in_use_by_daemon and not name_match:
161+
expected = list(images_in_use_by_daemon)[0]
162+
logger.warning(
163+
'Not using image %r of named daemon: %s',
164+
expected,
165+
reason,
166+
)
167+
if images_in_use and not ceph_match:
168+
expected = list(images_in_use)[0]
169+
logger.warning(
170+
'Not using image %r of ceph daemon: %s',
171+
expected,
172+
reason,
173+
)
174+
return best_image.name

src/cephadm/cephadmlib/listing.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,17 @@
5050
import os
5151
import logging
5252

53-
from typing import TypedDict, Union, Optional, Iterator, List, Any, Dict, cast
53+
from typing import (
54+
Any,
55+
Callable,
56+
Dict,
57+
Iterator,
58+
List,
59+
Optional,
60+
TypedDict,
61+
Union,
62+
cast,
63+
)
5464

5565
from .context import CephadmContext
5666
from .daemon_identity import DaemonIdentity
@@ -191,6 +201,7 @@ def daemons_matching(
191201
daemon_name: Optional[str] = None,
192202
daemon_type: Optional[str] = None,
193203
fsid: Optional[str] = None,
204+
daemon_type_predicate: Optional[Callable[[str], bool]] = None,
194205
) -> Iterator[Union[LegacyDaemonEntry, DaemonEntry]]:
195206
"""Iterate over the daemons configured on the current node, matching daemon
196207
name or daemon type if supplied.
@@ -201,6 +212,11 @@ def daemons_matching(
201212
continue
202213
if daemon_type is not None and daemon_type != entry.daemon_type:
203214
continue
215+
if (
216+
daemon_type_predicate is not None
217+
and not daemon_type_predicate(entry.daemon_type)
218+
):
219+
continue
204220
elif isinstance(entry, DaemonEntry):
205221
if fsid is not None and fsid != entry.identity.fsid:
206222
continue
@@ -214,6 +230,11 @@ def daemons_matching(
214230
and daemon_type != entry.identity.daemon_type
215231
):
216232
continue
233+
if (
234+
daemon_type_predicate is not None
235+
and not daemon_type_predicate(entry.identity.daemon_type)
236+
):
237+
continue
217238
else:
218239
raise ValueError(f'unexpected entry type: {entry}')
219240
yield entry

src/cephadm/tests/fixtures.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def with_cephadm_ctx(
169169
with contextlib.ExitStack() as stack:
170170
stack.enter_context(mock.patch('cephadmlib.net_utils.attempt_bind'))
171171
stack.enter_context(mock.patch('cephadmlib.exe_utils.find_executable', return_value='foo'))
172-
stack.enter_context(mock.patch('cephadm.get_container_info', return_value=None))
172+
stack.enter_context(mock.patch('cephadmlib.container_lookup.get_container_info', return_value=None))
173173
stack.enter_context(mock.patch('cephadm.is_available', return_value=True))
174174
stack.enter_context(mock.patch('cephadm.json_loads_retry', return_value={'epoch' : 1}))
175175
stack.enter_context(mock.patch('cephadm.logger'))

0 commit comments

Comments
 (0)