4
4
5
5
"""Charm for the filesystem client."""
6
6
7
- import json
8
7
import logging
9
- from collections import Counter
8
+ from dataclasses import dataclass
10
9
11
10
import ops
12
11
from charms .filesystem_client .v0 .filesystem_info import FilesystemRequires
13
- from jsonschema import ValidationError , validate
14
12
15
13
from utils .manager import MountsManager
16
14
17
- logger = logging .getLogger (__name__ )
18
-
19
- CONFIG_SCHEMA = {
20
- "$schema" : "http://json-schema.org/draft-04/schema#" ,
21
- "type" : "object" ,
22
- "additionalProperties" : {
23
- "type" : "object" ,
24
- "required" : ["mountpoint" ],
25
- "properties" : {
26
- "mountpoint" : {"type" : "string" },
27
- "noexec" : {"type" : "boolean" },
28
- "nosuid" : {"type" : "boolean" },
29
- "nodev" : {"type" : "boolean" },
30
- "read-only" : {"type" : "boolean" },
31
- },
32
- },
33
- }
15
+ _logger = logging .getLogger (__name__ )
34
16
35
17
36
18
class StopCharmError (Exception ):
37
19
"""Exception raised when a method needs to finish the execution of the charm code."""
38
20
39
- def __init__ (self , status : ops .StatusBase ):
21
+ def __init__ (self , status : ops .StatusBase ) -> None :
40
22
self .status = status
41
23
42
24
25
+ @dataclass (frozen = True )
26
+ class CharmConfig :
27
+ """Configuration for the charm."""
28
+
29
+ mountpoint : str
30
+ """Location to mount the filesystem on the machine."""
31
+ noexec : bool
32
+ """Block execution of binaries on the filesystem."""
33
+ nosuid : bool
34
+ """Do not honor suid and sgid bits on the filesystem."""
35
+ nodev : bool
36
+ """Blocking interpretation of character and/or block devices on the filesystem."""
37
+ read_only : bool
38
+ """Mount filesystem as read-only."""
39
+
40
+
43
41
# Trying to use a delta charm (one method per event) proved to be a bit unwieldy, since
44
42
# we would have to handle multiple updates at once:
45
43
# - mount requests
@@ -51,11 +49,11 @@ def __init__(self, status: ops.StatusBase):
51
49
# mount requests.
52
50
#
53
51
# A holistic charm (one method for all events) was a lot easier to deal with,
54
- # simplifying the code to handle all the multiple relations .
52
+ # simplifying the code to handle all the events .
55
53
class FilesystemClientCharm (ops .CharmBase ):
56
54
"""Charm the application."""
57
55
58
- def __init__ (self , framework : ops .Framework ):
56
+ def __init__ (self , framework : ops .Framework ) -> None :
59
57
super ().__init__ (framework )
60
58
61
59
self ._filesystems = FilesystemRequires (self , "filesystem" )
@@ -66,71 +64,66 @@ def __init__(self, framework: ops.Framework):
66
64
framework .observe (self ._filesystems .on .mount_filesystem , self ._handle_event )
67
65
framework .observe (self ._filesystems .on .umount_filesystem , self ._handle_event )
68
66
69
- def _handle_event (self , event : ops .EventBase ) -> None : # noqa: C901
67
+ def _handle_event (self , event : ops .EventBase ) -> None :
68
+ """Handle a Juju event."""
70
69
try :
71
70
self .unit .status = ops .MaintenanceStatus ("Updating status." )
72
71
72
+ # CephFS is not supported on LXD containers.
73
+ if not self ._mounts_manager .supported ():
74
+ self .unit .status = ops .BlockedStatus ("Cannot mount filesystems on LXD containers." )
75
+ return
76
+
73
77
self ._ensure_installed ()
74
78
config = self ._get_config ()
75
79
self ._mount_filesystems (config )
76
80
except StopCharmError as e :
77
81
# This was the cleanest way to ensure the inner methods can still return prematurely
78
82
# when an error occurs.
79
- self .app .status = e .status
83
+ self .unit .status = e .status
80
84
return
81
85
82
- self .unit .status = ops .ActiveStatus ("Mounted filesystems ." )
86
+ self .unit .status = ops .ActiveStatus (f "Mounted filesystem at ` { config . mountpoint } ` ." )
83
87
84
- def _ensure_installed (self ):
88
+ def _ensure_installed (self ) -> None :
85
89
"""Ensure the required packages are installed into the unit."""
86
90
if not self ._mounts_manager .installed :
87
91
self .unit .status = ops .MaintenanceStatus ("Installing required packages." )
88
92
self ._mounts_manager .install ()
89
93
90
- def _get_config (self ) -> dict [ str , dict [ str , str | bool ]] :
94
+ def _get_config (self ) -> CharmConfig :
91
95
"""Get and validate the configuration of the charm."""
92
- try :
93
- config = json .loads (str (self .config .get ("mounts" , "" )))
94
- validate (config , CONFIG_SCHEMA )
95
- config : dict [str , dict [str , str | bool ]] = config
96
- for fs , opts in config .items ():
97
- for opt in ["noexec" , "nosuid" , "nodev" , "read-only" ]:
98
- opts [opt ] = opts .get (opt , False )
99
- return config
100
- except (json .JSONDecodeError , ValidationError ) as e :
96
+ if not (mountpoint := self .config .get ("mountpoint" )):
97
+ raise StopCharmError (ops .BlockedStatus ("Missing `mountpoint` in config." ))
98
+
99
+ return CharmConfig (
100
+ mountpoint = str (mountpoint ),
101
+ noexec = bool (self .config .get ("noexec" )),
102
+ nosuid = bool (self .config .get ("nosuid" )),
103
+ nodev = bool (self .config .get ("nodev" )),
104
+ read_only = bool (self .config .get ("read-only" )),
105
+ )
106
+
107
+ def _mount_filesystems (self , config : CharmConfig ) -> None :
108
+ """Mount the filesystem for the charm."""
109
+ endpoints = self ._filesystems .endpoints
110
+ if not endpoints :
101
111
raise StopCharmError (
102
- ops .BlockedStatus (f"invalid configuration for option `mounts`. reason: \n { e } " )
112
+ ops .BlockedStatus ("Waiting for an integration with a filesystem provider. " )
103
113
)
104
114
105
- def _mount_filesystems (self , config : dict [str , dict [str , str | bool ]]):
106
- """Mount all available filesystems for the charm."""
107
- endpoints = self ._filesystems .endpoints
108
- for fs_type , count in Counter (
109
- [endpoint .info .filesystem_type () for endpoint in endpoints ]
110
- ).items ():
111
- if count > 1 :
112
- raise StopCharmError (
113
- ops .BlockedStatus (f"Too many relations for mount type `{ fs_type } `." )
114
- )
115
+ # This is limited to 1 relation.
116
+ endpoint = endpoints [0 ]
115
117
116
- self .unit .status = ops .MaintenanceStatus ("Ensuring filesystems are mounted ." )
118
+ self .unit .status = ops .MaintenanceStatus ("Mounting filesystem ." )
117
119
118
120
with self ._mounts_manager .mounts () as mounts :
119
- for endpoint in endpoints :
120
- fs_type = endpoint .info .filesystem_type ()
121
- if not (options := config .get (fs_type )):
122
- raise StopCharmError (
123
- ops .BlockedStatus (f"Missing configuration for mount type `{ fs_type } `." )
124
- )
125
-
126
- mountpoint = str (options ["mountpoint" ])
127
-
128
- opts = []
129
- opts .append ("noexec" if options .get ("noexec" ) else "exec" )
130
- opts .append ("nosuid" if options .get ("nosuid" ) else "suid" )
131
- opts .append ("nodev" if options .get ("nodev" ) else "dev" )
132
- opts .append ("ro" if options .get ("read-only" ) else "rw" )
133
- mounts .add (info = endpoint .info , mountpoint = mountpoint , options = opts )
121
+ opts = []
122
+ opts .append ("noexec" if config .noexec else "exec" )
123
+ opts .append ("nosuid" if config .nosuid else "suid" )
124
+ opts .append ("nodev" if config .nodev else "dev" )
125
+ opts .append ("ro" if config .read_only else "rw" )
126
+ mounts .add (info = endpoint .info , mountpoint = config .mountpoint , options = opts )
134
127
135
128
136
129
if __name__ == "__main__" : # pragma: nocover
0 commit comments