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()`
113 from pathlib
import Path
114 from typing
import List
116 from lightkube
import Client
117 from lightkube
.models
.core_v1
import HostPathVolumeSource
, Volume
, VolumeMount
118 from lightkube
.resources
.apps_v1
import StatefulSet
119 from ops
.charm
import CharmBase
120 from ops
.framework
import Object
, StoredState
121 from ops
.model
import (
129 from ops
.pebble
import ServiceStatus
131 # The unique Charmhub library identifier, never change it
132 LIBID
= "e915908eebee4cdd972d484728adf984"
134 # Increment this major API version when introducing breaking changes
137 # Increment this PATCH version before using `charmcraft publish-lib` or reset
138 # to 0 if you are raising the major API version
141 logger
= logging
.getLogger(__name__
)
144 class CharmError(Exception):
145 """Charm Error Exception."""
147 def __init__(self
, message
: str, status_class
: StatusBase
= BlockedStatus
) -> None:
148 self
.message
= message
149 self
.status_class
= status_class
150 self
.status
= status_class(message
)
153 def check_container_ready(container
: Container
) -> None:
154 """Check Pebble has started in the container.
157 container (Container): Container to be checked.
160 CharmError: if container is not ready.
162 if not container
.can_connect():
163 raise CharmError("waiting for pebble to start", MaintenanceStatus
)
166 def check_service_active(container
: Container
, service_name
: str) -> None:
167 """Check if the service is running.
170 container (Container): Container to be checked.
171 service_name (str): Name of the service to check.
174 CharmError: if the service is not running.
176 if service_name
not in container
.get_plan().services
:
177 raise CharmError(f
"{service_name} service not configured yet", WaitingStatus
)
179 if container
.get_service(service_name
).current
!= ServiceStatus
.ACTIVE
:
180 raise CharmError(f
"{service_name} service is not running")
183 def get_pod_ip() -> str:
184 """Get Kubernetes Pod IP.
187 str: The IP of the Pod.
189 return socket
.gethostbyname(socket
.gethostname())
192 _DEBUG_SCRIPT
= r
"""#!/bin/bash
195 function download_code(){{
196 wget https://go.microsoft.com/fwlink/?LinkID=760868 -O code.deb
199 function setup_envs(){{
200 grep "source /debug.envs" /root/.bashrc || echo "source /debug.envs" | tee -a /root/.bashrc
202 function setup_ssh(){{
204 cat /etc/ssh/sshd_config |
205 grep -E '^PermitRootLogin yes$$' || (
206 echo PermitRootLogin yes |
207 tee -a /etc/ssh/sshd_config
212 usermod --password $(echo {} | openssl passwd -1 -stdin) root
215 function setup_code(){{
216 apt install libasound2 -y
217 (dpkg -i code.deb || apt-get install -f -y || apt-get install -f -y) && echo Code installed successfully
218 code --install-extension ms-python.python --user-data-dir /root
219 mkdir -p /root/.vscode-server
220 cp -R /root/.vscode/extensions /root/.vscode-server/extensions
223 export DEBIAN_FRONTEND=noninteractive
224 apt update && apt install wget -y
235 """Represents a hostpath."""
237 def __init__(self
, config
: str, container_path
: str) -> None:
238 mount_path_items
= config
.split("-")
239 mount_path_items
.reverse()
240 self
.mount_path
= "/" + "/".join(mount_path_items
)
242 self
.container_path
= container_path
243 self
.module_name
= container_path
.split("/")[-1]
246 class DebugMode(Object
):
247 """Class to handle the debug-mode."""
253 container
: Container
,
254 hostpaths
: List
[HostPath
] = [],
255 vscode_workspace_path
: str = "files/vscode-workspace.json",
257 super().__init
__(charm
, "debug-mode")
260 self
._stored
= stored
261 self
.hostpaths
= hostpaths
262 self
.vscode_workspace
= Path(vscode_workspace_path
).read_text()
263 self
.container
= container
265 self
._stored
.set_default(
266 debug_mode_started
=False,
267 debug_mode_vscode_command
=None,
268 debug_mode_password
=None,
271 self
.framework
.observe(self
.charm
.on
.config_changed
, self
._on
_config
_changed
)
272 self
.framework
.observe(self
.charm
.on
[container
.name
].pebble_ready
, self
._on
_config
_changed
)
273 self
.framework
.observe(self
.charm
.on
.update_status
, self
._on
_update
_status
)
275 def _on_config_changed(self
, _
) -> None:
276 """Handler for the config-changed event."""
277 if not self
.charm
.unit
.is_leader():
280 debug_mode_enabled
= self
.charm
.config
.get("debug-mode", False)
281 action
= self
.enable
if debug_mode_enabled
else self
.disable
284 def _on_update_status(self
, _
) -> None:
285 """Handler for the update-status event."""
286 if not self
.charm
.unit
.is_leader() or not self
.started
:
289 self
.charm
.unit
.status
= ActiveStatus("debug-mode: ready")
292 def started(self
) -> bool:
293 """Indicates whether the debug-mode has started or not."""
294 return self
._stored
.debug_mode_started
297 def command(self
) -> str:
298 """Command to launch vscode."""
299 return self
._stored
.debug_mode_vscode_command
302 def password(self
) -> str:
304 return self
._stored
.debug_mode_password
306 def enable(self
, service_name
: str = None) -> None:
307 """Enable debug-mode.
309 This function mounts hostpaths of the OSM modules (if set), and
310 configures the container so it can be easily debugged. The setup
311 includes the configuration of SSH, environment variables, and
312 VSCode workspace and plugins.
315 service_name (str, optional): Pebble service name which has the desired environment
316 variables. Mandatory if there is more than one Pebble service configured.
318 hostpaths_to_reconfigure
= self
._hostpaths
_to
_reconfigure
()
319 if self
.started
and not hostpaths_to_reconfigure
:
320 self
.charm
.unit
.status
= ActiveStatus("debug-mode: ready")
323 logger
.debug("enabling debug-mode")
325 # Mount hostpaths if set.
326 # If hostpaths are mounted, the statefulset will be restarted,
327 # and for that reason we return immediately. On restart, the hostpaths
328 # won't be mounted and then we can continue and setup the debug-mode.
329 if hostpaths_to_reconfigure
:
330 self
.charm
.unit
.status
= MaintenanceStatus("debug-mode: configuring hostpaths")
331 self
._configure
_hostpaths
(hostpaths_to_reconfigure
)
334 self
.charm
.unit
.status
= MaintenanceStatus("debug-mode: starting")
335 password
= secrets
.token_hex(8)
336 self
._setup
_debug
_mode
(
339 mounted_hostpaths
=[hp
for hp
in self
.hostpaths
if self
.charm
.config
.get(hp
.config
)],
342 self
._stored
.debug_mode_vscode_command
= self
._get
_vscode
_command
(get_pod_ip())
343 self
._stored
.debug_mode_password
= password
344 self
._stored
.debug_mode_started
= True
345 logger
.info("debug-mode is ready")
346 self
.charm
.unit
.status
= ActiveStatus("debug-mode: ready")
348 def disable(self
) -> None:
349 """Disable debug-mode."""
350 logger
.debug("disabling debug-mode")
351 current_status
= self
.charm
.unit
.status
352 hostpaths_unmounted
= self
._unmount
_hostpaths
()
354 if not self
._stored
.debug_mode_started
:
356 self
._stored
.debug_mode_started
= False
357 self
._stored
.debug_mode_vscode_command
= None
358 self
._stored
.debug_mode_password
= None
360 if not hostpaths_unmounted
:
361 self
.charm
.unit
.status
= current_status
364 def _hostpaths_to_reconfigure(self
) -> List
[HostPath
]:
365 hostpaths_to_reconfigure
: List
[HostPath
] = []
367 statefulset
= client
.get(StatefulSet
, self
.charm
.app
.name
, namespace
=self
.charm
.model
.name
)
368 volumes
= statefulset
.spec
.template
.spec
.volumes
370 for hostpath
in self
.hostpaths
:
371 hostpath_is_set
= True if self
.charm
.config
.get(hostpath
.config
) else False
372 hostpath_already_configured
= next(
373 (True for volume
in volumes
if volume
.name
== hostpath
.config
), False
375 if hostpath_is_set
!= hostpath_already_configured
:
376 hostpaths_to_reconfigure
.append(hostpath
)
378 return hostpaths_to_reconfigure
380 def _setup_debug_mode(
383 service_name
: str = None,
384 mounted_hostpaths
: List
[HostPath
] = [],
386 services
= self
.container
.get_plan().services
387 if not service_name
and len(services
) != 1:
388 raise Exception("Cannot start debug-mode: please set the service_name")
392 service_name
, service
= services
.popitem()
394 service
= services
.get(service_name
)
396 logger
.debug(f
"getting environment variables from service {service_name}")
397 environment
= service
.environment
398 environment_file_content
= "\n".join(
399 [f
'export {key}="{value}"' for key
, value
in environment
.items()]
401 logger
.debug(f
"pushing environment file to {self.container.name} container")
402 self
.container
.push("/debug.envs", environment_file_content
)
404 # Push VSCode workspace
405 logger
.debug(f
"pushing vscode workspace to {self.container.name} container")
406 self
.container
.push("/debug.code-workspace", self
.vscode_workspace
)
408 # Execute debugging script
409 logger
.debug(f
"pushing debug-mode setup script to {self.container.name} container")
410 self
.container
.push("/debug.sh", _DEBUG_SCRIPT
.format(password
), permissions
=0o777)
411 logger
.debug(f
"executing debug-mode setup script in {self.container.name} container")
412 self
.container
.exec(["/debug.sh"]).wait_output()
413 logger
.debug(f
"stopping service {service_name} in {self.container.name} container")
414 self
.container
.stop(service_name
)
416 # Add symlinks to mounted hostpaths
417 for hostpath
in mounted_hostpaths
:
418 logger
.debug(f
"adding symlink for {hostpath.config}")
419 self
.container
.exec(["rm", "-rf", hostpath
.container_path
]).wait_output()
424 f
"{hostpath.mount_path}/{hostpath.module_name}",
425 hostpath
.container_path
,
429 def _configure_hostpaths(self
, hostpaths
: List
[HostPath
]):
431 statefulset
= client
.get(StatefulSet
, self
.charm
.app
.name
, namespace
=self
.charm
.model
.name
)
433 for hostpath
in hostpaths
:
434 if self
.charm
.config
.get(hostpath
.config
):
435 self
._add
_hostpath
_to
_statefulset
(hostpath
, statefulset
)
437 self
._delete
_hostpath
_from
_statefulset
(hostpath
, statefulset
)
439 client
.replace(statefulset
)
441 def _unmount_hostpaths(self
) -> bool:
443 hostpath_unmounted
= False
444 statefulset
= client
.get(StatefulSet
, self
.charm
.app
.name
, namespace
=self
.charm
.model
.name
)
446 for hostpath
in self
.hostpaths
:
447 if self
._delete
_hostpath
_from
_statefulset
(hostpath
, statefulset
):
448 hostpath_unmounted
= True
450 if hostpath_unmounted
:
451 client
.replace(statefulset
)
453 return hostpath_unmounted
455 def _add_hostpath_to_statefulset(self
, hostpath
: HostPath
, statefulset
: StatefulSet
):
457 logger
.debug(f
"adding volume {hostpath.config} to {self.charm.app.name} statefulset")
460 hostPath
=HostPathVolumeSource(
461 path
=self
.charm
.config
[hostpath
.config
],
465 statefulset
.spec
.template
.spec
.volumes
.append(volume
)
468 for statefulset_container
in statefulset
.spec
.template
.spec
.containers
:
469 if statefulset_container
.name
!= self
.container
.name
:
473 f
"adding volumeMount {hostpath.config} to {self.container.name} container"
475 statefulset_container
.volumeMounts
.append(
476 VolumeMount(mountPath
=hostpath
.mount_path
, name
=hostpath
.config
)
479 def _delete_hostpath_from_statefulset(self
, hostpath
: HostPath
, statefulset
: StatefulSet
):
480 hostpath_unmounted
= False
481 for volume
in statefulset
.spec
.template
.spec
.volumes
:
483 if hostpath
.config
!= volume
.name
:
487 for statefulset_container
in statefulset
.spec
.template
.spec
.containers
:
488 if statefulset_container
.name
!= self
.container
.name
:
490 for volume_mount
in statefulset_container
.volumeMounts
:
491 if volume_mount
.name
!= hostpath
.config
:
495 f
"removing volumeMount {hostpath.config} from {self.container.name} container"
497 statefulset_container
.volumeMounts
.remove(volume_mount
)
501 f
"removing volume {hostpath.config} from {self.charm.app.name} statefulset"
503 statefulset
.spec
.template
.spec
.volumes
.remove(volume
)
505 hostpath_unmounted
= True
506 return hostpath_unmounted
508 def _get_vscode_command(
512 workspace_path
: str = "/debug.code-workspace",
514 return f
"code --remote ssh-remote+{user}@{pod_ip} {workspace_path}"
517 self
.container
.exec(["kill", "-HUP", "1"])