Skip to content
Snippets Groups Projects
Commit 93f1539f authored by Mark Beierl's avatar Mark Beierl
Browse files

Virtual PC Descriptor


A simple descriptor that uses a native charm to launch a clean
Ubuntu base image and layer a graphical desktop environment
and RDP server on top.

Signed-off-by: default avatarbeierlm <mark.beierl@canonical.com>
parent 5ea55214
No related branches found
No related tags found
1 merge request!118Virtual PC Descriptor
Pipeline #187 passed with stage
in 1 minute and 39 seconds
Showing
with 1082 additions and 0 deletions
This diff is collapsed.
# virtual-pc
## Description
## Usage
### Prepare the environment
```bash
sudo snap install juju --classic --channel 2.8/stable
sudo snap install lxd
lxd.init
juju bootstrap lxd
juju add-model test-virtual-pc
```
### Deploy (from the Store)
```bash
juju deploy cs:~charmed-osm/virtual-pc --channel edge
```
### Deploy (locally)
Build the charm:
```bash
virtualenv -p python3 venv
source venv/bin/activate
pip install -r requirements-dev.txt
pip install charmcraft
./venv/bin/charmcraft build
```
Deploy:
```bash
juju deploy ./virtual-pc.charm
```
## Developing
Create and activate a virtualenv with the development requirements:
virtualenv -p python3 venv
source venv/bin/activate
pip install -r requirements-dev.txt
## Testing
The Python operator framework includes a very nice harness for testing
operator behaviour without full deployment. Just `run_tests`:
./run_tests
# Copyright 2020 ETSI OSM Contributors
# See LICENSE file for licensing details.
#
# This is only an example, and you should edit to suit your needs.
# If you don't need actions, you can remove the file entirely.
# It ties in to the example _on_fortune_action handler in src/charm.py
# Copyright 2020 ETSI OSM Contributors
# See LICENSE file for licensing details.
#
# This is only an example, and you should edit to suit your needs.
# If you don't need config, you can remove the file entirely.
options:
#!/bin/sh
JUJU_DISPATCH_PATH="${JUJU_DISPATCH_PATH:-$0}" PYTHONPATH=lib:venv ./src/charm.py
../dispatch
\ No newline at end of file
../dispatch
\ No newline at end of file
../dispatch
\ No newline at end of file
# Copyright 2020 David Garcia
# See LICENSE file for licensing details.
name: virtual-pc
description: |
TODO: fill out the charm's description
summary: |
TODO: fill out the charm's summary
series:
- focal
-r requirements.txt
flake8
charmcraft
\ No newline at end of file
ops
#!/bin/sh -e
# Copyright 2020 David Garcia
# See LICENSE file for licensing details.
if [ -z "$VIRTUAL_ENV" -a -d venv/ ]; then
. venv/bin/activate
fi
if [ -z "$PYTHONPATH" ]; then
export PYTHONPATH=src
else
export PYTHONPATH="src:$PYTHONPATH"
fi
flake8
python3 -m unittest -v "$@"
#!/usr/bin/env python3
# Copyright 2020 David Garcia
# See LICENSE file for licensing details.
from apt.progress.base import InstallProgress
import logging
import os
import shutil
from jinja2 import Template
from ops.charm import CharmBase
from ops.framework import StoredState
from ops.main import main
from ops.model import (
MaintenanceStatus,
ActiveStatus,
# BlockedStatus,
)
from utils import (
service_stop,
service_restart,
install_apt,
shell,
)
# from typing import Dict, Any
logger = logging.getLogger(__name__)
APT_REQUIREMENTS = [
"firefox",
"mate-desktop", # 469 packages
"mate-applets",
"mate-applet-brisk-menu",
"mate-indicator-applet",
"mate-session-manager",
"mate-terminal",
"xrdp",
]
SNAP_INSTALLS = [
"code --classic",
]
POLKIT_TEMPLATE = "./templates/color.pkla"
POLKIT_PATH = "/etc/polkit-1/localauthority/50-local.d/color.pkla"
STARTWM_TEMPLATE = "./templates/startwm.sh"
STARTWM_PATH = "/etc/xrdp/startwm.sh"
# WM_COMMAND = "startxfce4" # xubuntu-desktop
# WM_COMMAND = "budgie-desktop" # budgie-desktop-environment
WM_COMMAND = "mate-session" # mate-desktop
class VirtualPCCharm(CharmBase, InstallProgress):
_stored = StoredState()
def __init__(self, *args):
super().__init__(*args)
InstallProgress.__init__(self)
self._stored.set_default()
# Basic hooks
self.framework.observe(self.on.install, self._on_install)
self.framework.observe(self.on.start, self._on_start)
self.framework.observe(self.on.stop, self._on_stop)
self.framework.observe(self.on.config_changed, self._on_config_changed)
self.framework.observe(self.on.update_status, self._on_update_status)
# Actions hooks
# Relations hooks
# Override InstallProgress to update our status
def status_change(self, pkg, percent, status):
message = str(int(percent)) + "% " + status
self.unit.status = MaintenanceStatus(message)
# Basic hooks
def _on_install(self, _):
self.unit.status = MaintenanceStatus("Installing apt packages")
install_apt(packages=APT_REQUIREMENTS, update=True, progress=self)
service_stop('xrdp')
self.unit.status = MaintenanceStatus("Installing snaps")
for snap in SNAP_INSTALLS:
shell("sudo snap install " + snap)
self.unit.status = MaintenanceStatus("Setting default display manager")
shell("echo /usr/sbin/lightdm | sudo tee /etc/X11/default-display-manager")
self.unit.status = MaintenanceStatus("Adding XRDP to ssl-cert group")
shell("sudo adduser xrdp ssl-cert")
self.unit.status = MaintenanceStatus("Generating Window Manager startup script")
with open(STARTWM_TEMPLATE, "r") as template:
content = Template(template.read()).render(command=WM_COMMAND)
with open(STARTWM_PATH, "w") as startwm:
startwm.write(content)
self.unit.status = MaintenanceStatus("Generating Polkit files")
with open(POLKIT_TEMPLATE, "r") as template:
content = Template(template.read()).render()
with open(POLKIT_PATH, "w") as polkit:
polkit.write(content)
self._stored.installed = True
def _on_start(self, _):
self.unit.status = MaintenanceStatus("Starting XRDP server")
service_restart('xrdp')
self._stored.started = True
self.unit.status = self._get_current_status()
def _on_stop(self, _):
service_stop('xrdp')
self._stored.started = False
self.unit.status = self._get_current_status()
def _on_config_changed(self, _):
self.unit.status = self._get_current_status()
def _on_update_status(self, _):
self.unit.status = self._get_current_status()
# Action hooks
# Relation hooks
# Private functions
def _get_current_status(self):
status_type = ActiveStatus
status_msg = ""
if self._stored.installed:
status_msg = "Ready"
return status_type(status_msg)
if __name__ == "__main__":
main(VirtualPCCharm)
from apt.progress.base import InstallProgress
from utils import (
install_apt,
)
class Progress(InstallProgress):
def status_change(self, pkg, percent, status):
print("status change\n")
#print(str(int(percent)) + "% \n")
True
if __name__ == "__main__":
install_apt(packages=["mate-backgrounds"], update=True, progress=Progress())
import apt
from apt.progress.base import OpProgress
import shutil
import subprocess
from typing import Dict, List, NoReturn
def service_active(service_name: str):
result = subprocess.run(
["systemctl", "is-active", service_name],
stdout=subprocess.PIPE,
encoding="utf-8",
)
return result.stdout == "active\n"
def all_values_set(dictionary: Dict[str, str]) -> bool:
return not any(v is None for v in dictionary.values())
def install_apt(packages: List, update: bool = False, progress=None) -> NoReturn:
cache = apt.cache.Cache()
if update:
cache.update()
cache.open()
for package in packages:
pkg = cache[package]
if not pkg.is_installed:
pkg.mark_install()
cache.commit(install_progress=progress)
def remove_apt(packages: List, update: bool = False) -> NoReturn:
cache = apt.cache.Cache()
if update:
cache.update()
cache.open()
for package in packages:
pkg = cache[package]
if not pkg.is_installed:
pkg.mark_delete()
cache.commit()
def shell(command: str) -> NoReturn:
subprocess.run(command, shell=True).check_returncode()
def copy_files(origin: Dict[str, str], destination: Dict[str, str]) -> NoReturn:
for config, origin_path in origin.items():
destination_path = destination[config]
shutil.copy(origin_path, destination_path)
# Service functions
def _systemctl(action: str, service_name: str) -> NoReturn:
subprocess.run(["systemctl", action, service_name]).check_returncode()
def service_start(service_name: str) -> NoReturn:
_systemctl("start", service_name)
def service_restart(service_name: str) -> NoReturn:
_systemctl("restart", service_name)
def service_stop(service_name: str) -> NoReturn:
_systemctl("stop", service_name)
def service_enable(service_name: str) -> NoReturn:
_systemctl("enable", service_name)
def systemctl_daemon_reload():
subprocess.run(["systemctl", "daemon-reload"]).check_returncode()
[Allow colord for all users]
Identity=unix-user:*
Action=org.freedesktop.color-manager.create-device;org.freedesktop.color-manager.create-profile;org.freedesktop.color-manager.delete-device;org.freedesktop.color-manager.delete-profile;org.freedesktop.color-manager.modify-device;org.freedesktop.color-manager.modify-profile
ResultAny=yes
ResultInactive=yes
ResultActive=yes
\ No newline at end of file
#!/bin/sh
# xrdp X session start script (c) 2015, 2017 mirabilos
# published under The MirOS Licence
if test -r /etc/profile; then
. /etc/profile
fi
if test -r /etc/default/locale; then
. /etc/default/locale
test -z "${LANG+x}" || export LANG
test -z "${LANGUAGE+x}" || export LANGUAGE
test -z "${LC_ADDRESS+x}" || export LC_ADDRESS
test -z "${LC_ALL+x}" || export LC_ALL
test -z "${LC_COLLATE+x}" || export LC_COLLATE
test -z "${LC_CTYPE+x}" || export LC_CTYPE
test -z "${LC_IDENTIFICATION+x}" || export LC_IDENTIFICATION
test -z "${LC_MEASUREMENT+x}" || export LC_MEASUREMENT
test -z "${LC_MESSAGES+x}" || export LC_MESSAGES
test -z "${LC_MONETARY+x}" || export LC_MONETARY
test -z "${LC_NAME+x}" || export LC_NAME
test -z "${LC_NUMERIC+x}" || export LC_NUMERIC
test -z "${LC_PAPER+x}" || export LC_PAPER
test -z "${LC_TELEPHONE+x}" || export LC_TELEPHONE
test -z "${LC_TIME+x}" || export LC_TIME
test -z "${LOCPATH+x}" || export LOCPATH
fi
if test -r /etc/profile; then
. /etc/profile
fi
{{ command }}
# Copyright 2020 David Garcia
# See LICENSE file for licensing details.
import unittest
from unittest.mock import Mock
from ops.testing import Harness
from charm import SrsLteCharm
class TestCharm(unittest.TestCase):
def test_config_changed(self):
harness = Harness(SrsLteCharm)
self.addCleanup(harness.cleanup)
harness.begin()
self.assertEqual(list(harness.charm._stored.things), [])
harness.update_config({"thing": "foo"})
self.assertEqual(list(harness.charm._stored.things), ["foo"])
def test_action(self):
harness = Harness(SrsLteCharm)
harness.begin()
# the harness doesn't (yet!) help much with actions themselves
action_event = Mock(params={"fail": ""})
harness.charm._on_fortune_action(action_event)
self.assertTrue(action_event.set_results.called)
def test_action_fail(self):
harness = Harness(SrsLteCharm)
harness.begin()
action_event = Mock(params={"fail": "fail this"})
harness.charm._on_fortune_action(action_event)
self.assertEqual(action_event.fail.call_args, [("fail this",)])
File added
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment