Compare commits

...

8 Commits

Author SHA1 Message Date
Mike Degatano
c2123f0903 Ensure addon.start always returns coroutine (#4409) 2023-06-27 19:43:49 +02:00
Mike Degatano
9fbeb2a769 Add support for apparmor channels (#4408) 2023-06-27 19:12:42 +02:00
dependabot[bot]
3e0723ec24 Bump sigstore/cosign-installer from 3.0.5 to 3.1.0 (#4407)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-27 10:17:53 +02:00
Pascal Vizeli
3e5f1d96b5 Fix Tag (#4406) 2023-06-26 18:49:13 +02:00
dependabot[bot]
be87082502 Bump home-assistant/builder from 2023.06.0 to 2023.06.1 (#4404)
* Bump home-assistant/builder from 2023.06.0 to 2023.06.1

Bumps [home-assistant/builder](https://github.com/home-assistant/builder) from 2023.06.0 to 2023.06.1.
- [Release notes](https://github.com/home-assistant/builder/releases)
- [Commits](https://github.com/home-assistant/builder/compare/2023.06.0...2023.06.1)

---
updated-dependencies:
- dependency-name: home-assistant/builder
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* Support cosign & ditch Codenotary

* use curl

* Not using docker.io anymore

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Pascal Vizeli <pvizeli@syshack.ch>
2023-06-26 18:17:55 +02:00
dependabot[bot]
f997e51249 Bump pytest from 7.3.2 to 7.4.0 (#4405)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-26 12:00:01 +02:00
dependabot[bot]
456316fdd4 Bump sentry-sdk from 1.25.1 to 1.26.0 (#4401)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-23 16:57:36 +02:00
Mike Degatano
9a7d547394 Allow all characters in mount credentials (#4399)
* Allow all characters in mount credentials

* Fix permissions on credential files

* Fix pylint issue
2023-06-22 15:55:13 -04:00
19 changed files with 158 additions and 96 deletions

View File

@@ -83,6 +83,10 @@ jobs:
name: Build ${{ matrix.arch }} supervisor
needs: init
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
packages: write
strategy:
matrix:
arch: ${{ fromJson(needs.init.outputs.architectures) }}
@@ -119,12 +123,29 @@ jobs:
with:
type: ${{ env.BUILD_TYPE }}
- name: Login to DockerHub
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.publish == 'true'
uses: docker/login-action@v2.2.0
uses: actions/setup-python@v4.6.1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Install Cosign
if: needs.init.outputs.publish == 'true'
uses: sigstore/cosign-installer@v3.1.0
with:
cosign-release: "v2.0.2"
- name: Install dirhash and calc hash
if: needs.init.outputs.publish == 'true'
run: |
pip3 install dirhash
dir_hash="$(dirhash "${{ github.workspace }}/supervisor" -a sha256 --match "*.py")"
echo "${dir_hash}" > rootfs/supervisor.sha256
- name: Sign supervisor SHA256
if: needs.init.outputs.publish == 'true'
run: |
cosign sign-blob --yes rootfs/supervisor.sha256 --bundle rootfs/supervisor.sha256.sig
- name: Login to GitHub Container Registry
if: needs.init.outputs.publish == 'true'
@@ -139,55 +160,17 @@ jobs:
run: echo "BUILD_ARGS=--test" >> $GITHUB_ENV
- name: Build supervisor
uses: home-assistant/builder@2023.06.0
uses: home-assistant/builder@2023.06.1
with:
args: |
$BUILD_ARGS \
--${{ matrix.arch }} \
--target /data \
--cosign \
--generic ${{ needs.init.outputs.version }}
env:
CAS_API_KEY: ${{ secrets.CAS_TOKEN }}
codenotary:
name: CAS signature
needs: init
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
if: needs.init.outputs.publish == 'true'
uses: actions/checkout@v3.5.3
with:
fetch-depth: 0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.publish == 'true'
uses: actions/setup-python@v4.6.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Set version
if: needs.init.outputs.publish == 'true'
uses: home-assistant/actions/helpers/version@master
with:
type: ${{ env.BUILD_TYPE }}
- name: Install dirhash and calc hash
if: needs.init.outputs.publish == 'true'
id: dirhash
run: |
pip3 install dirhash
dir_hash="$(dirhash "${{ github.workspace }}/supervisor" -a sha256 --match "*.py")"
echo "::set-output name=dirhash::${dir_hash}"
- name: Signing Source
if: needs.init.outputs.publish == 'true'
uses: home-assistant/actions/helpers/codenotary@master
with:
source: hash://${{ steps.dirhash.outputs.dirhash }}
asset: supervisor-${{ needs.init.outputs.version }}
token: ${{ secrets.CAS_TOKEN }}
version:
name: Update version
needs: ["init", "run_supervisor"]
@@ -216,7 +199,7 @@ jobs:
run_supervisor:
runs-on: ubuntu-latest
name: Run the Supervisor
needs: ["build", "codenotary", "init"]
needs: ["build", "init"]
timeout-minutes: 60
steps:
- name: Checkout the repository
@@ -224,7 +207,7 @@ jobs:
- name: Build the Supervisor
if: needs.init.outputs.publish != 'true'
uses: home-assistant/builder@2023.06.0
uses: home-assistant/builder@2023.06.1
with:
args: |
--test \
@@ -236,7 +219,7 @@ jobs:
if: needs.init.outputs.publish == 'true'
run: |
docker pull ghcr.io/home-assistant/amd64-hassio-supervisor:${{ needs.init.outputs.version }}
docker tag ghcr.io/home-assistant/amd64-hassio-supervisor:${{ needs.init.outputs.version }} homeassistant/amd64-hassio-supervisor:runner
docker tag ghcr.io/home-assistant/amd64-hassio-supervisor:${{ needs.init.outputs.version }} ghcr.io/home-assistant/amd64-hassio-supervisor:runner
- name: Create the Supervisor
run: |
@@ -253,7 +236,7 @@ jobs:
-e SUPERVISOR_NAME=hassio_supervisor \
-e SUPERVISOR_DEV=1 \
-e SUPERVISOR_MACHINE="qemux86-64" \
homeassistant/amd64-hassio-supervisor:runner
ghcr.io/home-assistant/amd64-hassio-supervisor:runner
- name: Start the Supervisor
run: docker start hassio_supervisor

View File

@@ -10,7 +10,6 @@ on:
env:
DEFAULT_PYTHON: "3.11"
PRE_COMMIT_HOME: ~/.cache/pre-commit
DEFAULT_CAS: v1.0.2
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
@@ -351,10 +350,10 @@ jobs:
id: python
with:
python-version: ${{ needs.prepare.outputs.python-version }}
- name: Install CAS tools
uses: home-assistant/actions/helpers/cas@master
- name: Install Cosign
uses: sigstore/cosign-installer@v3.1.0
with:
version: ${{ env.DEFAULT_CAS }}
cosign-release: "v2.0.2"
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@v3.3.1

View File

@@ -7,7 +7,8 @@ ENV \
CRYPTOGRAPHY_OPENSSL_NO_LEGACY=1
ARG \
CAS_VERSION
COSIGN_VERSION \
BUILD_ARCH
# Install base
WORKDIR /usr/src
@@ -21,19 +22,9 @@ RUN \
libpulse \
musl \
openssl \
&& apk add --no-cache --virtual .build-dependencies \
build-base \
go \
\
&& git clone -b "v${CAS_VERSION}" --depth 1 \
https://github.com/codenotary/cas \
&& cd cas \
&& make cas \
&& mv cas /usr/bin/cas \
\
&& apk del .build-dependencies \
&& rm -rf /root/go /root/.cache \
&& rm -rf /usr/src/cas
&& curl -Lso /usr/bin/cosign "https://github.com/home-assistant/cosign/releases/download/${COSIGN_VERSION}/cosign_${BUILD_ARCH}" \
&& chmod a+x /usr/bin/cosign
# Install requirements
COPY requirements.txt .

View File

@@ -1,5 +1,4 @@
image: homeassistant/{arch}-hassio-supervisor
shadow_repository: ghcr.io/home-assistant
image: ghcr.io/home-assistant/{arch}-hassio-supervisor
build_from:
aarch64: ghcr.io/home-assistant/aarch64-base-python:3.11-alpine3.16
armhf: ghcr.io/home-assistant/armhf-base-python:3.11-alpine3.16
@@ -9,8 +8,11 @@ build_from:
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io
cosign:
base_identity: https://github.com/home-assistant/docker-base/.*
identity: https://github.com/home-assistant/supervisor/.*
args:
CAS_VERSION: 1.0.2
COSIGN_VERSION: 2.0.2
labels:
io.hass.type: supervisor
org.opencontainers.image.title: Home Assistant Supervisor

View File

@@ -20,7 +20,7 @@ pulsectl==23.5.2
pyudev==0.24.1
ruamel.yaml==0.17.21
securetar==2023.3.0
sentry-sdk==1.25.1
sentry-sdk==1.26.0
voluptuous==0.13.1
dbus-fast==1.86.0
typing_extensions==4.6.3

View File

@@ -9,7 +9,7 @@ pytest-aiohttp==1.0.4
pytest-asyncio==0.18.3
pytest-cov==4.1.0
pytest-timeout==2.1.0
pytest==7.3.2
pytest==7.4.0
pyupgrade==3.7.0
time-machine==2.10.0
typing_extensions==4.6.3

View File

@@ -1,4 +0,0 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE03LvYuz79GTJx4uKp3w6NrSe5JZI
iBtgzzYi0YQYtZO/r+xFpgDJEa0gLHkXtl94fpqrFiN89In83lzaszbZtA==
-----END PUBLIC KEY-----

View File

@@ -1,8 +0,0 @@
{
"currentcontext": {
"LcHost": "cas.codenotary.com",
"LcPort": "443"
},
"schemaversion": 3,
"users": null
}

View File

@@ -716,7 +716,7 @@ class Addon(AddonModel):
"""
if await self.instance.is_running():
_LOGGER.warning("%s is already running!", self.slug)
return
return self._wait_for_startup()
# Access Token
self.persist[ATTR_ACCESS_TOKEN] = secrets.token_hex(56)

View File

@@ -202,11 +202,18 @@ def initialize_system(coresys: CoreSys) -> None:
_LOGGER.debug("Creating Supervisor media folder at '%s'", config.path_media)
config.path_media.mkdir()
# Mounts folder
# Mounts folders
if not config.path_mounts.is_dir():
_LOGGER.debug("Creating Supervisor mounts folder at '%s'", config.path_mounts)
config.path_mounts.mkdir()
if not config.path_mounts_credentials.is_dir():
_LOGGER.debug(
"Creating Supervisor mounts credentials folder at '%s'",
config.path_mounts_credentials,
)
config.path_mounts_credentials.mkdir(mode=0o600)
# Emergency folder
if not config.path_emergency.is_dir():
_LOGGER.debug(

View File

@@ -46,6 +46,7 @@ DNS_DATA = PurePath("dns")
AUDIO_DATA = PurePath("audio")
MEDIA_DATA = PurePath("media")
MOUNTS_FOLDER = PurePath("mounts")
MOUNTS_CREDENTIALS = PurePath(".mounts_credentials")
EMERGENCY_DATA = PurePath("emergency")
DEFAULT_BOOT_TIME = datetime.utcfromtimestamp(0).isoformat()
@@ -315,6 +316,16 @@ class CoreConfig(FileConfiguration):
"""Return mounts path external for Docker."""
return self.path_extern_supervisor / MOUNTS_FOLDER
@property
def path_mounts_credentials(self) -> Path:
"""Return mounts credentials folder."""
return self.path_supervisor / MOUNTS_CREDENTIALS
@property
def path_extern_mounts_credentials(self) -> PurePath:
"""Return mounts credentials path external for Docker."""
return self.path_extern_supervisor / MOUNTS_CREDENTIALS
@property
def path_emergency(self) -> Path:
"""Return emergency data folder."""

View File

@@ -10,7 +10,7 @@ SUPERVISOR_VERSION = "99.9.9dev"
SERVER_SOFTWARE = f"HomeAssistantSupervisor/{SUPERVISOR_VERSION} aiohttp/{aiohttpversion} Python/{systemversion[0]}.{systemversion[1]}"
URL_HASSIO_ADDONS = "https://github.com/home-assistant/addons"
URL_HASSIO_APPARMOR = "https://version.home-assistant.io/apparmor.txt"
URL_HASSIO_APPARMOR = "https://version.home-assistant.io/apparmor_{channel}.txt"
URL_HASSIO_VERSION = "https://version.home-assistant.io/{channel}.json"
SUPERVISOR_DATA = Path("/data")

View File

@@ -388,12 +388,38 @@ class CIFSMount(NetworkMount):
options.append(f"vers={self.version}")
if self.username and self.password:
options.extend([f"username={self.username}", f"password={self.password}"])
options.append(f"credentials={self.path_extern_credentials.as_posix()}")
else:
options.append("guest")
return options
@property
def path_credentials(self) -> Path:
"""Path to credentials file."""
return self.sys_config.path_mounts_credentials / self.name
@property
def path_extern_credentials(self) -> PurePath:
"""Path to credentials file external to Docker."""
return self.sys_config.path_extern_mounts_credentials / self.name
async def mount(self) -> None:
"""Mount using systemd."""
if self.username and self.password:
if not self.path_credentials.exists():
self.path_credentials.touch(mode=0o600)
with self.path_credentials.open(mode="w") as cred_file:
cred_file.write(f"username={self.username}\npassword={self.password}")
await super().mount()
async def unmount(self) -> None:
"""Unmount using systemd."""
self.path_credentials.unlink(missing_ok=True)
await super().unmount()
class NFSMount(NetworkMount):
"""An NFS type mount."""

View File

@@ -34,8 +34,6 @@ RE_MOUNT_OPTION = re.compile(r"^[^,=]+$")
VALIDATE_NAME = vol.Match(RE_MOUNT_NAME)
VALIDATE_SERVER = vol.Match(RE_PATH_PART)
VALIDATE_SHARE = vol.Match(RE_PATH_PART)
VALIDATE_USERNAME = vol.Match(RE_MOUNT_OPTION)
VALIDATE_PASSWORD = vol.Match(RE_MOUNT_OPTION)
_SCHEMA_BASE_MOUNT_CONFIG = vol.Schema(
{
@@ -57,8 +55,8 @@ SCHEMA_MOUNT_CIFS = _SCHEMA_MOUNT_NETWORK.extend(
{
vol.Required(ATTR_TYPE): MountType.CIFS.value,
vol.Required(ATTR_SHARE): VALIDATE_SHARE,
vol.Inclusive(ATTR_USERNAME, "basic_auth"): VALIDATE_USERNAME,
vol.Inclusive(ATTR_PASSWORD, "basic_auth"): VALIDATE_PASSWORD,
vol.Inclusive(ATTR_USERNAME, "basic_auth"): str,
vol.Inclusive(ATTR_PASSWORD, "basic_auth"): str,
vol.Optional(ATTR_VERSION, default=None): vol.Maybe(
vol.Coerce(MountCifsVersion)
),

View File

@@ -115,7 +115,7 @@ class Supervisor(CoreSysAttributes):
async def update_apparmor(self) -> None:
"""Fetch last version and update profile."""
url = URL_HASSIO_APPARMOR
url = URL_HASSIO_APPARMOR.format(channel=self.sys_updater.channel.value)
# Fetch
try:

View File

@@ -471,3 +471,22 @@ async def test_restore(
start_task = await coresys.addons.restore(TEST_ADDON_SLUG, tarfile)
assert bool(start_task) is (status == "running")
async def test_start_when_running(
coresys: CoreSys,
install_addon_ssh: Addon,
container: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test starting an addon without healthcheck."""
container.status = "running"
await install_addon_ssh.load()
assert install_addon_ssh.state == AddonState.STARTED
caplog.clear()
start_task = await install_addon_ssh.start()
assert start_task
await start_task
assert "local_ssh is already running" in caplog.text

View File

@@ -371,6 +371,7 @@ async def tmp_supervisor_data(coresys: CoreSys, tmp_path: Path) -> Path:
coresys.config.path_emergency.mkdir()
coresys.config.path_media.mkdir()
coresys.config.path_mounts.mkdir()
coresys.config.path_mounts_credentials.mkdir()
coresys.config.path_backup.mkdir()
coresys.config.path_tmp.mkdir()
coresys.config.path_homeassistant.mkdir()

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import os
from pathlib import Path
import stat
from typing import Any
from unittest.mock import patch
@@ -78,8 +79,7 @@ async def test_cifs_mount(
assert mount.where == Path("/mnt/data/supervisor/mounts/test")
assert mount.local_where == tmp_supervisor_data / "mounts" / "test"
assert mount.options == ["noserverino"] + expected_options + [
f"username={mount_data['username']}",
f"password={mount_data['password']}",
"credentials=/mnt/data/supervisor/.mounts_credentials/test",
]
assert not mount.local_where.exists()
@@ -107,8 +107,7 @@ async def test_cifs_mount(
["noserverino"]
+ expected_options
+ [
f"username={mount_data['username']}",
f"password={mount_data['password']}",
"credentials=/mnt/data/supervisor/.mounts_credentials/test"
]
),
),
@@ -120,6 +119,19 @@ async def test_cifs_mount(
[],
)
]
assert mount.path_credentials.exists()
with mount.path_credentials.open("r") as creds:
assert creds.read().split("\n") == [
f"username={mount_data['username']}",
f"password={mount_data['password']}",
]
cred_stat = mount.path_credentials.stat()
assert not cred_stat.st_mode & stat.S_IRGRP
assert not cred_stat.st_mode & stat.S_IROTH
await mount.unmount()
assert not mount.path_credentials.exists()
async def test_nfs_mount(
@@ -279,7 +291,7 @@ async def test_unmount(
systemd_service: SystemdService = all_dbus_services["systemd"]
systemd_service.StopUnit.calls.clear()
mount = Mount.from_dict(
mount: CIFSMount = Mount.from_dict(
coresys,
{
"name": "test",

View File

@@ -3,13 +3,16 @@
from datetime import timedelta
from unittest.mock import AsyncMock, Mock, PropertyMock, patch
from aiohttp import ClientTimeout
from aiohttp.client_exceptions import ClientError
from awesomeversion import AwesomeVersion
import pytest
from supervisor.const import UpdateChannel
from supervisor.coresys import CoreSys
from supervisor.docker.supervisor import DockerSupervisor
from supervisor.exceptions import DockerError, SupervisorUpdateError
from supervisor.host.apparmor import AppArmorControl
from supervisor.resolution.const import ContextType, IssueType
from supervisor.resolution.data import Issue
from supervisor.supervisor import Supervisor
@@ -83,3 +86,25 @@ async def test_update_failed(coresys: CoreSys, capture_exception: Mock):
Issue(IssueType.UPDATE_FAILED, ContextType.SUPERVISOR)
in coresys.resolution.issues
)
@pytest.mark.parametrize(
"channel", [UpdateChannel.STABLE, UpdateChannel.BETA, UpdateChannel.DEV]
)
async def test_update_apparmor(
coresys: CoreSys, channel: UpdateChannel, tmp_supervisor_data
):
"""Test updating apparmor."""
coresys.updater.channel = channel
with patch("supervisor.coresys.aiohttp.ClientSession.get") as get, patch.object(
AppArmorControl, "load_profile"
) as load_profile:
get.return_value.__aenter__.return_value.status = 200
get.return_value.__aenter__.return_value.text = AsyncMock(return_value="")
await coresys.supervisor.update_apparmor()
get.assert_called_once_with(
f"https://version.home-assistant.io/apparmor_{channel.value}.txt",
timeout=ClientTimeout(total=10),
)
load_profile.assert_called_once()