2 # Copyright 2022 Canonical Ltd.
3 # See LICENSE file for licensing details.
4 # http://www.apache.org/licenses/LICENSE-2.0
7 This library offers some utilities made for but not limited to Charmed OSM.
11 Execute the following command inside your Charmed Operator folder to fetch the library.
14 charmcraft fetch-lib charms.osm_libs.v0.utils
17 # CharmError Exception
19 An exception that takes to arguments, the message and the StatusBase class, which are useful
20 to set the status of the charm when the exception raises.
24 from charms.osm_libs.v0.utils import CharmError
26 class MyCharm(CharmBase):
27 def _on_config_changed(self, _):
29 if not self.config.get("some-option"):
30 raise CharmError("need some-option", BlockedStatus)
32 if not self.mysql_ready:
33 raise CharmError("waiting for mysql", WaitingStatus)
37 exception CharmError as e:
38 self.unit.status = e.status
43 The `check_container_ready` function checks that a container is ready,
44 and therefore Pebble is ready.
46 The `check_service_active` function checks that a service in a container is running.
48 Both functions raise a CharmError if the validations fail.
52 from charms.osm_libs.v0.utils import check_container_ready, check_service_active
54 class MyCharm(CharmBase):
55 def _on_config_changed(self, _):
57 container: Container = self.unit.get_container("my-container")
58 check_container_ready(container)
59 check_service_active(container, "my-service")
62 exception CharmError as e:
63 self.unit.status = e.status
68 The debug-mode allows OSM developers to easily debug OSM modules.
72 from charms.osm_libs.v0.utils import DebugMode
74 class MyCharm(CharmBase):
75 _stored = StoredState()
77 def __init__(self, _):
79 container: Container = self.unit.get_container("my-container")
82 config="module-hostpath",
83 container_path="/usr/lib/python3/dist-packages/module"
86 vscode_workspace_path = "files/vscode-workspace.json"
87 self.debug_mode = DebugMode(
92 vscode_workspace_path,
95 def _on_update_status(self, _):
96 if self.debug_mode.started:
100 def _get_debug_mode_information(self):
101 command = self.debug_mode.command
102 password = self.debug_mode.password
103 return command, password
108 - Get pod IP with `get_pod_ip()`
110 from dataclasses
import dataclass
114 from pathlib
import Path
115 from typing
import List
117 from lightkube
import Client
118 from lightkube
.models
.core_v1
import HostPathVolumeSource
, Volume
, VolumeMount
119 from lightkube
.resources
.apps_v1
import StatefulSet
120 from ops
.charm
import CharmBase
121 from ops
.framework
import Object
, StoredState
122 from ops
.model
import (
130 from ops
.pebble
import ServiceStatus
132 # The unique Charmhub library identifier, never change it
133 LIBID
= "e915908eebee4cdd972d484728adf984"
135 # Increment this major API version when introducing breaking changes
138 # Increment this PATCH version before using `charmcraft publish-lib` or reset
139 # to 0 if you are raising the major API version
142 logger
= logging
.getLogger(__name__
)
145 class CharmError(Exception):
146 """Charm Error Exception."""
148 def __init__(self
, message
: str, status_class
: StatusBase
= BlockedStatus
) -> None:
149 self
.message
= message
150 self
.status_class
= status_class
151 self
.status
= status_class(message
)
154 def check_container_ready(container
: Container
) -> None:
155 """Check Pebble has started in the container.
158 container (Container): Container to be checked.
161 CharmError: if container is not ready.
163 if not container
.can_connect():
164 raise CharmError("waiting for pebble to start", MaintenanceStatus
)
167 def check_service_active(container
: Container
, service_name
: str) -> None:
168 """Check if the service is running.
171 container (Container): Container to be checked.
172 service_name (str): Name of the service to check.
175 CharmError: if the service is not running.
177 if service_name
not in container
.get_plan().services
:
178 raise CharmError(f
"{service_name} service not configured yet", WaitingStatus
)
180 if container
.get_service(service_name
).current
!= ServiceStatus
.ACTIVE
:
181 raise CharmError(f
"{service_name} service is not running")
184 def get_pod_ip() -> str:
185 """Get Kubernetes Pod IP.
188 str: The IP of the Pod.
190 return socket
.gethostbyname(socket
.gethostname())
193 _DEBUG_SCRIPT
= r
"""#!/bin/bash
196 function download_code(){{
197 wget https://go.microsoft.com/fwlink/?LinkID=760868 -O code.deb
200 function setup_envs(){{
201 grep "source /debug.envs" /root/.bashrc || echo "source /debug.envs" | tee -a /root/.bashrc
203 function setup_ssh(){{
205 cat /etc/ssh/sshd_config |
206 grep -E '^PermitRootLogin yes$$' || (
207 echo PermitRootLogin yes |
208 tee -a /etc/ssh/sshd_config
213 usermod --password $(echo {} | openssl passwd -1 -stdin) root
216 function setup_code(){{
217 apt install libasound2 -y
218 (dpkg -i code.deb || apt-get install -f -y || apt-get install -f -y) && echo Code installed successfully
219 code --install-extension ms-python.python --user-data-dir /root
220 mkdir -p /root/.vscode-server
221 cp -R /root/.vscode/extensions /root/.vscode-server/extensions
224 export DEBIAN_FRONTEND=noninteractive
225 apt update && apt install wget -y
237 """Represent RO Submodules."""
243 """Represents a hostpath."""
244 def __init__(self
, config
: str, container_path
: str, submodules
: dict = None) -> None:
245 mount_path_items
= config
.split("-")
246 mount_path_items
.reverse()
247 self
.mount_path
= "/" + "/".join(mount_path_items
)
249 self
.sub_module_dict
= {}
251 for submodule
in submodules
.keys():
252 self
.sub_module_dict
[submodule
] = SubModule(
253 sub_module_path
=self
.mount_path
+ "/" + submodule
+ "/" + submodules
[submodule
].split("/")[-1],
254 container_path
=submodules
[submodule
],
257 self
.container_path
= container_path
258 self
.module_name
= container_path
.split("/")[-1]
260 class DebugMode(Object
):
261 """Class to handle the debug-mode."""
267 container
: Container
,
268 hostpaths
: List
[HostPath
] = [],
269 vscode_workspace_path
: str = "files/vscode-workspace.json",
271 super().__init
__(charm
, "debug-mode")
274 self
._stored
= stored
275 self
.hostpaths
= hostpaths
276 self
.vscode_workspace
= Path(vscode_workspace_path
).read_text()
277 self
.container
= container
279 self
._stored
.set_default(
280 debug_mode_started
=False,
281 debug_mode_vscode_command
=None,
282 debug_mode_password
=None,
285 self
.framework
.observe(self
.charm
.on
.config_changed
, self
._on
_config
_changed
)
286 self
.framework
.observe(self
.charm
.on
[container
.name
].pebble_ready
, self
._on
_config
_changed
)
287 self
.framework
.observe(self
.charm
.on
.update_status
, self
._on
_update
_status
)
289 def _on_config_changed(self
, _
) -> None:
290 """Handler for the config-changed event."""
291 if not self
.charm
.unit
.is_leader():
294 debug_mode_enabled
= self
.charm
.config
.get("debug-mode", False)
295 action
= self
.enable
if debug_mode_enabled
else self
.disable
298 def _on_update_status(self
, _
) -> None:
299 """Handler for the update-status event."""
300 if not self
.charm
.unit
.is_leader() or not self
.started
:
303 self
.charm
.unit
.status
= ActiveStatus("debug-mode: ready")
306 def started(self
) -> bool:
307 """Indicates whether the debug-mode has started or not."""
308 return self
._stored
.debug_mode_started
311 def command(self
) -> str:
312 """Command to launch vscode."""
313 return self
._stored
.debug_mode_vscode_command
316 def password(self
) -> str:
318 return self
._stored
.debug_mode_password
320 def enable(self
, service_name
: str = None) -> None:
321 """Enable debug-mode.
323 This function mounts hostpaths of the OSM modules (if set), and
324 configures the container so it can be easily debugged. The setup
325 includes the configuration of SSH, environment variables, and
326 VSCode workspace and plugins.
329 service_name (str, optional): Pebble service name which has the desired environment
330 variables. Mandatory if there is more than one Pebble service configured.
332 hostpaths_to_reconfigure
= self
._hostpaths
_to
_reconfigure
()
333 if self
.started
and not hostpaths_to_reconfigure
:
334 self
.charm
.unit
.status
= ActiveStatus("debug-mode: ready")
337 logger
.debug("enabling debug-mode")
339 # Mount hostpaths if set.
340 # If hostpaths are mounted, the statefulset will be restarted,
341 # and for that reason we return immediately. On restart, the hostpaths
342 # won't be mounted and then we can continue and setup the debug-mode.
343 if hostpaths_to_reconfigure
:
344 self
.charm
.unit
.status
= MaintenanceStatus("debug-mode: configuring hostpaths")
345 self
._configure
_hostpaths
(hostpaths_to_reconfigure
)
348 self
.charm
.unit
.status
= MaintenanceStatus("debug-mode: starting")
349 password
= secrets
.token_hex(8)
350 self
._setup
_debug
_mode
(
353 mounted_hostpaths
=[hp
for hp
in self
.hostpaths
if self
.charm
.config
.get(hp
.config
)],
356 self
._stored
.debug_mode_vscode_command
= self
._get
_vscode
_command
(get_pod_ip())
357 self
._stored
.debug_mode_password
= password
358 self
._stored
.debug_mode_started
= True
359 logger
.info("debug-mode is ready")
360 self
.charm
.unit
.status
= ActiveStatus("debug-mode: ready")
362 def disable(self
) -> None:
363 """Disable debug-mode."""
364 logger
.debug("disabling debug-mode")
365 current_status
= self
.charm
.unit
.status
366 hostpaths_unmounted
= self
._unmount
_hostpaths
()
368 if not self
._stored
.debug_mode_started
:
370 self
._stored
.debug_mode_started
= False
371 self
._stored
.debug_mode_vscode_command
= None
372 self
._stored
.debug_mode_password
= None
374 if not hostpaths_unmounted
:
375 self
.charm
.unit
.status
= current_status
378 def _hostpaths_to_reconfigure(self
) -> List
[HostPath
]:
379 hostpaths_to_reconfigure
: List
[HostPath
] = []
381 statefulset
= client
.get(StatefulSet
, self
.charm
.app
.name
, namespace
=self
.charm
.model
.name
)
382 volumes
= statefulset
.spec
.template
.spec
.volumes
384 for hostpath
in self
.hostpaths
:
385 hostpath_is_set
= True if self
.charm
.config
.get(hostpath
.config
) else False
386 hostpath_already_configured
= next(
387 (True for volume
in volumes
if volume
.name
== hostpath
.config
), False
389 if hostpath_is_set
!= hostpath_already_configured
:
390 hostpaths_to_reconfigure
.append(hostpath
)
392 return hostpaths_to_reconfigure
394 def _setup_debug_mode(
397 service_name
: str = None,
398 mounted_hostpaths
: List
[HostPath
] = [],
400 services
= self
.container
.get_plan().services
401 if not service_name
and len(services
) != 1:
402 raise Exception("Cannot start debug-mode: please set the service_name")
406 service_name
, service
= services
.popitem()
408 service
= services
.get(service_name
)
410 logger
.debug(f
"getting environment variables from service {service_name}")
411 environment
= service
.environment
412 environment_file_content
= "\n".join(
413 [f
'export {key}="{value}"' for key
, value
in environment
.items()]
415 logger
.debug(f
"pushing environment file to {self.container.name} container")
416 self
.container
.push("/debug.envs", environment_file_content
)
418 # Push VSCode workspace
419 logger
.debug(f
"pushing vscode workspace to {self.container.name} container")
420 self
.container
.push("/debug.code-workspace", self
.vscode_workspace
)
422 # Execute debugging script
423 logger
.debug(f
"pushing debug-mode setup script to {self.container.name} container")
424 self
.container
.push("/debug.sh", _DEBUG_SCRIPT
.format(password
), permissions
=0o777)
425 logger
.debug(f
"executing debug-mode setup script in {self.container.name} container")
426 self
.container
.exec(["/debug.sh"]).wait_output()
427 logger
.debug(f
"stopping service {service_name} in {self.container.name} container")
428 self
.container
.stop(service_name
)
430 # Add symlinks to mounted hostpaths
431 for hostpath
in mounted_hostpaths
:
432 logger
.debug(f
"adding symlink for {hostpath.config}")
433 if len(hostpath
.sub_module_dict
) > 0:
434 for sub_module
in hostpath
.sub_module_dict
.keys():
435 self
.container
.exec(["rm", "-rf", hostpath
.sub_module_dict
[sub_module
].container_path
]).wait_output()
440 hostpath
.sub_module_dict
[sub_module
].sub_module_path
,
441 hostpath
.sub_module_dict
[sub_module
].container_path
,
446 self
.container
.exec(["rm", "-rf", hostpath
.container_path
]).wait_output()
451 f
"{hostpath.mount_path}/{hostpath.module_name}",
452 hostpath
.container_path
,
456 def _configure_hostpaths(self
, hostpaths
: List
[HostPath
]):
458 statefulset
= client
.get(StatefulSet
, self
.charm
.app
.name
, namespace
=self
.charm
.model
.name
)
460 for hostpath
in hostpaths
:
461 if self
.charm
.config
.get(hostpath
.config
):
462 self
._add
_hostpath
_to
_statefulset
(hostpath
, statefulset
)
464 self
._delete
_hostpath
_from
_statefulset
(hostpath
, statefulset
)
466 client
.replace(statefulset
)
468 def _unmount_hostpaths(self
) -> bool:
470 hostpath_unmounted
= False
471 statefulset
= client
.get(StatefulSet
, self
.charm
.app
.name
, namespace
=self
.charm
.model
.name
)
473 for hostpath
in self
.hostpaths
:
474 if self
._delete
_hostpath
_from
_statefulset
(hostpath
, statefulset
):
475 hostpath_unmounted
= True
477 if hostpath_unmounted
:
478 client
.replace(statefulset
)
480 return hostpath_unmounted
482 def _add_hostpath_to_statefulset(self
, hostpath
: HostPath
, statefulset
: StatefulSet
):
484 logger
.debug(f
"adding volume {hostpath.config} to {self.charm.app.name} statefulset")
487 hostPath
=HostPathVolumeSource(
488 path
=self
.charm
.config
[hostpath
.config
],
492 statefulset
.spec
.template
.spec
.volumes
.append(volume
)
495 for statefulset_container
in statefulset
.spec
.template
.spec
.containers
:
496 if statefulset_container
.name
!= self
.container
.name
:
500 f
"adding volumeMount {hostpath.config} to {self.container.name} container"
502 statefulset_container
.volumeMounts
.append(
503 VolumeMount(mountPath
=hostpath
.mount_path
, name
=hostpath
.config
)
506 def _delete_hostpath_from_statefulset(self
, hostpath
: HostPath
, statefulset
: StatefulSet
):
507 hostpath_unmounted
= False
508 for volume
in statefulset
.spec
.template
.spec
.volumes
:
510 if hostpath
.config
!= volume
.name
:
514 for statefulset_container
in statefulset
.spec
.template
.spec
.containers
:
515 if statefulset_container
.name
!= self
.container
.name
:
517 for volume_mount
in statefulset_container
.volumeMounts
:
518 if volume_mount
.name
!= hostpath
.config
:
522 f
"removing volumeMount {hostpath.config} from {self.container.name} container"
524 statefulset_container
.volumeMounts
.remove(volume_mount
)
528 f
"removing volume {hostpath.config} from {self.charm.app.name} statefulset"
530 statefulset
.spec
.template
.spec
.volumes
.remove(volume
)
532 hostpath_unmounted
= True
533 return hostpath_unmounted
535 def _get_vscode_command(
539 workspace_path
: str = "/debug.code-workspace",
541 return f
"code --remote ssh-remote+{user}@{pod_ip} {workspace_path}"
544 self
.container
.exec(["kill", "-HUP", "1"])