diff --git a/API.md b/API.md index 98748b4dc..550225982 100644 --- a/API.md +++ b/API.md @@ -451,7 +451,6 @@ Get all available addons. "host_ipc": "bool", "host_dbus": "bool", "privileged": ["NET_ADMIN", "SYS_ADMIN"], - "seccomp": "disable|default|profile", "apparmor": "disable|default|profile", "devices": ["/dev/xy"], "auto_uart": "bool", diff --git a/Dockerfile b/Dockerfile index 2ed844f41..201d2d6b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ RUN apk add --no-cache \ python3-dev \ g++ \ && pip3 install --no-cache-dir \ - uvloop==0.9.1 \ + uvloop==0.10.1 \ cchardet==2.1.1 \ pycryptodome==3.4.11 \ && apk del .build-dependencies diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index 55fc10dc9..8edf45d8c 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -25,11 +25,12 @@ from ..const import ( ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, ATTR_HOST_IPC, ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES, - ATTR_SECCOMP, ATTR_APPARMOR, SECURITY_PROFILE, SECURITY_DISABLE, - SECURITY_DEFAULT) + ATTR_APPARMOR, SECURITY_PROFILE, SECURITY_DISABLE, SECURITY_DEFAULT) from ..coresys import CoreSysAttributes from ..docker.addon import DockerAddon from ..utils.json import write_json_file, read_json_file +from ..utils.apparmor import adjust_profile +from ..exceptions import HostAppArmorError _LOGGER = logging.getLogger(__name__) @@ -319,21 +320,12 @@ class Addon(CoreSysAttributes): """Return list of privilege.""" return self._mesh.get(ATTR_PRIVILEGED) - @property - def seccomp(self): - """Return True if seccomp is enabled.""" - if not self._mesh.get(ATTR_SECCOMP): - return SECURITY_DISABLE - elif self.path_seccomp.exists(): - return SECURITY_PROFILE - return SECURITY_DEFAULT - @property def apparmor(self): - """Return True if seccomp is enabled.""" + """Return True if apparmor is enabled.""" if not self._mesh.get(ATTR_APPARMOR): return SECURITY_DISABLE - elif self.path_apparmor.exists(): + elif self.sys_host.apparmor.exists(self.slug): return SECURITY_PROFILE return SECURITY_DEFAULT @@ -493,15 +485,10 @@ class Addon(CoreSysAttributes): """Return path to addon changelog.""" return Path(self.path_location, 'CHANGELOG.md') - @property - def path_seccomp(self): - """Return path to custom seccomp profile.""" - return Path(self.path_location, 'seccomp.json') - @property def path_apparmor(self): """Return path to custom AppArmor profile.""" - return Path(self.path_location, 'apparmor') + return Path(self.path_location, 'apparmor.txt') @property def path_asound(self): @@ -549,6 +536,27 @@ class Addon(CoreSysAttributes): return True + async def _install_apparmor(self): + """Install or Update AppArmor profile for Add-on.""" + exists_local = self.sys_host.apparmor.exists(self.slug) + exists_addon = self.path_apparmor.exists() + + # Nothing to do + if not exists_local and not exists_addon: + return + + # Need removed + if exists_local and not exists_addon: + await self.sys_host.apparmor.remove_profile(self.slug) + return + + # Need install/update + with TemporaryDirectory(dir=self.sys_config.path_tmp) as tmp_folder: + profile_file = Path(tmp_folder, 'apparmor.txt') + + adjust_profile(self.slug, self.path_apparmor, self.profile_file) + await self.sys_host.apparmor.load_profile(self.slug, profile_file) + @property def schema(self): """Create a schema for addon options.""" @@ -604,6 +612,9 @@ class Addon(CoreSysAttributes): "Create Home-Assistant addon data folder %s", self.path_data) self.path_data.mkdir() + # Setup/Fix AppArmor profile + await self._install_apparmor() + if not await self.instance.install(self.last_version): return False @@ -626,6 +637,11 @@ class Addon(CoreSysAttributes): with suppress(OSError): self.path_asound.unlink() + # Cleanup apparmor profile + if self.sys_host.apparmor.exists(self.slug): + with suppress(HostAppArmorError): + await self.sys_host.apparmor.remove_profile(self.slug) + self._set_uninstall() return True @@ -672,6 +688,9 @@ class Addon(CoreSysAttributes): return False self._set_update(self.last_version) + # Setup/Fix AppArmor profile + await self._install_apparmor() + # restore state if last_state == STATE_STARTED: await self.start() @@ -738,7 +757,7 @@ class Addon(CoreSysAttributes): with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp: # store local image if self.need_build and not await \ - self.instance.export_image(Path(temp, "image.tar")): + self.instance.export_image(Path(temp, 'image.tar')): return False data = { @@ -750,11 +769,20 @@ class Addon(CoreSysAttributes): # store local configs/state try: - write_json_file(Path(temp, "addon.json"), data) + write_json_file(Path(temp, 'addon.json'), data) except (OSError, json.JSONDecodeError) as err: _LOGGER.error("Can't save meta for %s: %s", self._id, err) return False + # Store AppArmor Profile + if self.sys_host.apparmor.exists(self.slug): + profile = Path(temp, 'apparmor.txt') + try: + self.sys_host.apparmor.backup_profile(self.slug, profile) + except HostAppArmorError: + _LOGGER.error("Can't backup AppArmor profile") + return False + # write into tarfile def _write_tarfile(): """Write tar inside loop.""" @@ -789,7 +817,7 @@ class Addon(CoreSysAttributes): # read snapshot data try: - data = read_json_file(Path(temp, "addon.json")) + data = read_json_file(Path(temp, 'addon.json')) except (OSError, json.JSONDecodeError) as err: _LOGGER.error("Can't read addon.json: %s", err) @@ -810,7 +838,7 @@ class Addon(CoreSysAttributes): if not await self.instance.exists(): _LOGGER.info("Restore image for addon %s", self._id) - image_file = Path(temp, "image.tar") + image_file = Path(temp, 'image.tar') if image_file.is_file(): await self.instance.import_image(image_file, version) else: @@ -833,6 +861,16 @@ class Addon(CoreSysAttributes): _LOGGER.error("Can't restore origin data: %s", err) return False + # Restore AppArmor + profile_file = Path(temp, 'apparmor.txt') + if profile_file.exists(): + try: + await self.sys_host.apparmor.load_profile( + self.slug, profile_file) + except HostAppArmorError: + _LOGGER.error("Can't restore AppArmor profile") + return False + # run addon if data[ATTR_STATE] == STATE_STARTED: return await self.start() diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index cc0fde405..822b448ba 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -18,7 +18,7 @@ from ..const import ( ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, ATTR_BUILD_FROM, ATTR_SQUASH, ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY, - ATTR_SECCOMP, ATTR_APPARMOR) + ATTR_APPARMOR) from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE _LOGGER = logging.getLogger(__name__) @@ -108,7 +108,6 @@ SCHEMA_ADDON_CONFIG = vol.Schema({ vol.Optional(ATTR_MAP, default=list): [vol.Match(RE_VOLUME)], vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)}, vol.Optional(ATTR_PRIVILEGED): [vol.In(PRIVILEGED_ALL)], - vol.Optional(ATTR_SECCOMP, default=True): vol.Boolean(), vol.Optional(ATTR_APPARMOR, default=True): vol.Boolean(), vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(), vol.Optional(ATTR_GPIO, default=False): vol.Boolean(), diff --git a/hassio/api/addons.py b/hassio/api/addons.py index aa3fbfbc8..ab31bbc1c 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -17,7 +17,7 @@ from ..const import ( ATTR_CHANGELOG, ATTR_HOST_IPC, ATTR_HOST_DBUS, ATTR_LONG_DESCRIPTION, ATTR_CPU_PERCENT, ATTR_MEMORY_LIMIT, ATTR_MEMORY_USAGE, ATTR_NETWORK_TX, ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_ICON, ATTR_SERVICES, - ATTR_DISCOVERY, ATTR_SECCOMP, ATTR_APPARMOR, + ATTR_DISCOVERY, ATTR_APPARMOR, CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT) from ..coresys import CoreSysAttributes from ..validate import DOCKER_PORTS, ALSA_DEVICE @@ -126,7 +126,6 @@ class APIAddons(CoreSysAttributes): ATTR_HOST_IPC: addon.host_ipc, ATTR_HOST_DBUS: addon.host_dbus, ATTR_PRIVILEGED: addon.privileged, - ATTR_SECCOMP: addon.seccomp, ATTR_APPARMOR: addon.apparmor, ATTR_DEVICES: self._pretty_devices(addon), ATTR_ICON: addon.with_icon, diff --git a/hassio/bootstrap.py b/hassio/bootstrap.py index 828d0cdb4..d35ccb207 100644 --- a/hassio/bootstrap.py +++ b/hassio/bootstrap.py @@ -103,6 +103,11 @@ def initialize_system_data(coresys): _LOGGER.info("Create hassio share folder %s", config.path_share) config.path_share.mkdir() + # apparmor folder + if not config.path_apparmor.is_dir(): + _LOGGER.info("Create hassio apparmor folder %s", config.path_apparmor) + config.path_apparmor.mkdir() + return config diff --git a/hassio/config.py b/hassio/config.py index 2f86ad5ed..6f1aafd5a 100644 --- a/hassio/config.py +++ b/hassio/config.py @@ -25,6 +25,7 @@ ADDONS_DATA = PurePath("addons/data") BACKUP_DATA = PurePath("backup") SHARE_DATA = PurePath("share") TMP_DATA = PurePath("tmp") +APPARMOR_DATA = PurePath("apparmor") DEFAULT_BOOT_TIME = datetime.utcfromtimestamp(0).isoformat() @@ -156,6 +157,11 @@ class CoreConfig(JsonConfig): """Return root share data folder.""" return Path(HASSIO_DATA, SHARE_DATA) + @property + def path_apparmor(self): + """Return root apparmor profile folder.""" + return Path(HASSIO_DATA, APPARMOR_DATA) + @property def path_extern_share(self): """Return root share data folder extern for docker.""" diff --git a/hassio/const.py b/hassio/const.py index 74415f29e..92e1629c4 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -7,6 +7,8 @@ HASSIO_VERSION = '108' URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons" URL_HASSIO_VERSION = \ "https://s3.amazonaws.com/hassio-version/{channel}.json" +URL_HASSIO_APPARMOR = \ + "https://s3.amazonaws.com/hassio-version/apparmor.txt" HASSIO_DATA = Path("/data") @@ -162,7 +164,6 @@ ATTR_PROTECTED = 'protected' ATTR_CRYPTO = 'crypto' ATTR_BRANCH = 'branch' ATTR_KERNEL = 'kernel' -ATTR_SECCOMP = 'seccomp' ATTR_APPARMOR = 'apparmor' SERVICE_MQTT = 'mqtt' diff --git a/hassio/coresys.py b/hassio/coresys.py index fe8ef696e..7242b9dd7 100644 --- a/hassio/coresys.py +++ b/hassio/coresys.py @@ -267,4 +267,4 @@ class CoreSysAttributes: """Mapping to coresys.""" if name.startswith("sys_") and hasattr(self.coresys, name[4:]): return getattr(self.coresys, name[4:]) - raise AttributeError() + raise AttributeError(f"Can't resolve {name} on {self}") diff --git a/hassio/docker/addon.py b/hassio/docker/addon.py index 960923c75..cba7e8870 100644 --- a/hassio/docker/addon.py +++ b/hassio/docker/addon.py @@ -124,18 +124,17 @@ class DockerAddon(DockerInterface): security = [] # AppArmor - if self.addon.apparmor == SECURITY_DISABLE: + apparmor = self.sys_host.apparmor.available + if not apparmor or self.addon.apparmor == SECURITY_DISABLE: security.append("apparmor:unconfined") elif self.addon.apparmor == SECURITY_PROFILE: security.append(f"apparmor={self.addon.slug}") - # Seccomp - if self.addon.seccomp == SECURITY_DISABLE: - security.append("seccomp=unconfined") - elif self.addon.seccomp == SECURITY_PROFILE: - security.append(f"seccomp={self.addon.path_seccomp}") + # Disable Seccomp / We don't support it official and it + # make troubles on some kind of host systems. + security.append("seccomp=unconfined") - return security or None + return security @property def tmpfs(self): diff --git a/hassio/exceptions.py b/hassio/exceptions.py index e3342ecf1..58dc044de 100644 --- a/hassio/exceptions.py +++ b/hassio/exceptions.py @@ -6,11 +6,6 @@ class HassioError(Exception): pass -class HassioInternalError(HassioError): - """Internal Hass.io error they can't handle.""" - pass - - class HassioNotSupportedError(HassioError): """Function is not supported.""" pass @@ -33,6 +28,10 @@ class HostServiceError(HostError): pass +class HostAppArmorError(HostError): + """Host apparmor functions fails.""" + + # utils/gdbus class DBusError(HassioError): @@ -52,3 +51,20 @@ class DBusFatalError(DBusError): class DBusParseError(DBusError): """DBus parse error.""" pass + + +# util/apparmor + +class AppArmorError(HostAppArmorError): + """General AppArmor error.""" + pass + + +class AppArmorFileError(AppArmorError): + """AppArmor profile file error.""" + pass + + +class AppArmorInvalidError(AppArmorError): + """AppArmor profile validate error.""" + pass diff --git a/hassio/host/__init__.py b/hassio/host/__init__.py index a65e46f32..de1bbd479 100644 --- a/hassio/host/__init__.py +++ b/hassio/host/__init__.py @@ -1,12 +1,18 @@ """Host function like audio/dbus/systemd.""" +from contextlib import suppress +import logging from .alsa import AlsaAudio +from .apparmor import AppArmorControl from .control import SystemControl from .info import InfoCenter -from .service import ServiceManager +from .services import ServiceManager from ..const import ( FEATURES_REBOOT, FEATURES_SHUTDOWN, FEATURES_HOSTNAME, FEATURES_SERVICES) from ..coresys import CoreSysAttributes +from ..exceptions import HassioError + +_LOGGER = logging.getLogger(__name__) class HostManager(CoreSysAttributes): @@ -16,6 +22,7 @@ class HostManager(CoreSysAttributes): """Initialize Host manager.""" self.coresys = coresys self._alsa = AlsaAudio(coresys) + self._apparmor = AppArmorControl(coresys) self._control = SystemControl(coresys) self._info = InfoCenter(coresys) self._services = ServiceManager(coresys) @@ -25,6 +32,11 @@ class HostManager(CoreSysAttributes): """Return host ALSA handler.""" return self._alsa + @property + def apparmor(self): + """Return host apparmor handler.""" + return self._apparmor + @property def control(self): """Return host control handler.""" @@ -57,14 +69,21 @@ class HostManager(CoreSysAttributes): return features - async def load(self): - """Load host functions.""" + async def reload(self): + """Reload host functions.""" if self.sys_dbus.hostname.is_connected: await self.info.update() if self.sys_dbus.systemd.is_connected: await self.services.update() - def reload(self): - """Reload host information.""" - return self.load() + async def load(self): + """Load host information.""" + with suppress(HassioError): + await self.reload() + + # Load profile data + try: + await self.apparmor.load() + except HassioError as err: + _LOGGER.waring("Load host AppArmor on start fails: %s", err) diff --git a/hassio/host/apparmor.py b/hassio/host/apparmor.py new file mode 100644 index 000000000..2b39368c2 --- /dev/null +++ b/hassio/host/apparmor.py @@ -0,0 +1,121 @@ +"""AppArmor control for host.""" +import logging +import shutil +from pathlib import Path + +from ..coresys import CoreSysAttributes +from ..exceptions import DBusError, HostAppArmorError +from ..utils.apparmor import validate_profile + +_LOGGER = logging.getLogger(__name__) + +SYSTEMD_SERVICES = {'hassos-apparmor.service', 'hassio-apparmor.service'} + + +class AppArmorControl(CoreSysAttributes): + """Handle host apparmor controls.""" + + def __init__(self, coresys): + """Initialize host power handling.""" + self.coresys = coresys + self._profiles = set() + self._service = None + + @property + def available(self): + """Return True if AppArmor is availabe on host.""" + return self._service is not None + + def exists(self, profile): + """Return True if a profile exists.""" + return profile in self._profiles + + async def _reload_service(self): + """Reload internal service.""" + try: + await self.sys_host.services.reload(self._service) + except DBusError as err: + _LOGGER.error("Can't reload %s: %s", self._service, err) + + def _get_profile(self, profile_name): + """Get a profile from AppArmor store.""" + if profile_name not in self._profiles: + _LOGGER.error("Can't find %s for removing", profile_name) + raise HostAppArmorError() + return Path(self.sys_config.path_apparmor, profile_name) + + async def load(self): + """Load available profiles.""" + for content in self.sys_config.path_apparmor.iterdir(): + if not content.is_file(): + continue + self._profiles.add(content.name) + + # Is connected with systemd? + _LOGGER.info("Load AppArmor Profiles: %s", self._profiles) + for service in SYSTEMD_SERVICES: + if not self.sys_host.services.exists(service): + continue + self._service = service + + # Load profiles + if self.available: + await self._reload_service() + else: + _LOGGER.info("AppArmor is not enabled on Host") + + async def load_profile(self, profile_name, profile_file): + """Load/Update a new/exists profile into AppArmor.""" + if not validate_profile(profile_file, profile_name): + _LOGGER.error("profile is not valid with name %s", profile_name) + raise HostAppArmorError() + + # Copy to AppArmor folder + dest_profile = Path(self.sys_config.path_apparmor, profile_name) + try: + shutil.copy(profile_file, dest_profile) + except OSError as err: + _LOGGER.error("Can't copy %s: %s", profile_file, err) + raise HostAppArmorError() from None + + # Load profiles + _LOGGER.info("Add or Update AppArmor profile: %s", profile_name) + self._profiles.add(profile_name) + if self.available: + await self._reload_service() + + async def remove_profile(self, profile_name): + """Remove a AppArmor profile.""" + profile_file = self._get_profile(profile_name) + + # Only remove file + if not self.available: + try: + profile_file.unlink() + except OSError as err: + _LOGGER.error("Can't remove profile: %s", err) + raise HostAppArmorError() + return + + # Marks als remove and start host process + remove_profile = Path( + self.sys_config.path_apparmor, 'remove', profile_name) + try: + profile_file.rename(remove_profile) + except OSError as err: + _LOGGER.error("Can't mark profile as remove: %s", err) + raise HostAppArmorError() + + _LOGGER.info("Remove AppArmor profile: %s", profile_name) + self._profiles.remove(profile_name) + await self._reload_service() + + def backup_profile(self, profile_name, backup_file): + """Backup A profile into a new file.""" + profile_file = self._get_profile(profile_name) + + try: + shutil.copy(profile_file, backup_file) + except OSError as err: + _LOGGER.error("Can't backup profile %s: %s", profile_name, err) + raise HostAppArmorError() diff --git a/hassio/host/service.py b/hassio/host/services.py similarity index 100% rename from hassio/host/service.py rename to hassio/host/services.py diff --git a/hassio/snapshots/validate.py b/hassio/snapshots/validate.py index 3ac714bbe..ab051ffbc 100644 --- a/hassio/snapshots/validate.py +++ b/hassio/snapshots/validate.py @@ -39,7 +39,7 @@ SCHEMA_SNAPSHOT = vol.Schema({ vol.Optional(ATTR_BOOT, default=True): vol.Boolean(), vol.Optional(ATTR_SSL, default=False): vol.Boolean(), vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT, - vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)), + vol.Optional(ATTR_PASSWORD): vol.Maybe(vol.Coerce(str)), vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(), vol.Optional(ATTR_WAIT_BOOT, default=600): vol.All(vol.Coerce(int), vol.Range(min=60)), diff --git a/hassio/supervisor.py b/hassio/supervisor.py index 598113b76..6d11f9483 100644 --- a/hassio/supervisor.py +++ b/hassio/supervisor.py @@ -1,8 +1,14 @@ """HomeAssistant control object.""" import logging +from pathlib import Path +from tempfile import TemporaryDirectory + +import aiohttp from .coresys import CoreSysAttributes from .docker.supervisor import DockerSupervisor +from .const import URL_HASSIO_APPARMOR +from .exceptions import HostAppArmorError _LOGGER = logging.getLogger(__name__) @@ -46,6 +52,31 @@ class Supervisor(CoreSysAttributes): """Return arch of hass.io containter.""" return self.instance.arch + async def update_apparmor(self): + """Fetch last version and update profile.""" + url = URL_HASSIO_APPARMOR + try: + _LOGGER.info("Fetch AppArmor profile %s", url) + async with self.sys_websession.get(url, timeout=10) as request: + data = await request.text() + + except aiohttp.ClientError as err: + _LOGGER.warning("Can't fetch AppArmor profile: %s", err) + return + + with TemporaryDirectory(dir=self.sys_config.path_tmp) as tmp_dir: + profile_file = Path(tmp_dir, 'apparmor.txt') + try: + profile_file.write_text(data) + except OSError as err: + _LOGGER.error("Can't write temporary profile: %s", err) + return + try: + await self.sys_host.apparmor.load_profile( + "hassio-supervisor", profile_file) + except HostAppArmorError: + _LOGGER.error("Can't update AppArmor profile!") + async def update(self, version=None): """Update HomeAssistant version.""" version = version or self.last_version @@ -56,6 +87,7 @@ class Supervisor(CoreSysAttributes): _LOGGER.info("Update supervisor to version %s", version) if await self.instance.install(version): + await self.update_apparmor() self.sys_loop.call_later(1, self.sys_loop.stop) return True diff --git a/hassio/tasks.py b/hassio/tasks.py index c9936d123..d5ddc298b 100644 --- a/hassio/tasks.py +++ b/hassio/tasks.py @@ -41,7 +41,7 @@ class Tasks(CoreSysAttributes): self.jobs.add(self.sys_scheduler.register_task( self.sys_snapshots.reload, self.RUN_RELOAD_SNAPSHOTS)) self.jobs.add(self.sys_scheduler.register_task( - self.sys_host.load, self.RUN_RELOAD_HOST)) + self.sys_host.reload, self.RUN_RELOAD_HOST)) self.jobs.add(self.sys_scheduler.register_task( self._watchdog_homeassistant_docker, diff --git a/hassio/utils/apparmor.py b/hassio/utils/apparmor.py new file mode 100644 index 000000000..aa80a4d59 --- /dev/null +++ b/hassio/utils/apparmor.py @@ -0,0 +1,66 @@ +"""Some functions around apparmor profiles.""" +import logging +import re + +from ..exceptions import AppArmorFileError, AppArmorInvalidError + +_LOGGER = logging.getLogger(__name__) + +RE_PROFILE = re.compile(r"^profile ([^ ]+).*$") + + +def get_profile_name(profile_file): + """Read the profile name from file.""" + profiles = set() + + try: + with profile_file.open('r') as profile: + for line in profile: + match = RE_PROFILE.match(line) + if not match: + continue + profiles.add(match.group(1)) + except OSError as err: + _LOGGER.error("Can't read apparmor profile: %s", err) + raise AppArmorFileError() + + if len(profiles) != 1: + _LOGGER.error("To many profiles inside file: %s", profiles) + raise AppArmorInvalidError() + + return profiles.pop() + + +def validate_profile(profile_file, profile_name): + """Check if profile from file is valid with profile name.""" + if profile_name == get_profile_name(profile_file): + return True + return False + + +def adjust_profile(profile_file, profile_name, profile_new): + """Fix the profile name.""" + org_profile = get_profile_name(profile_file) + profile_data = [] + + # Process old data + try: + with profile_file.open('r') as profile: + for line in profile: + match = RE_PROFILE.match(line) + if not match: + profile_data.append(line) + else: + profile_data.append( + line.replace(org_profile, profile_name)) + except OSError as err: + _LOGGER.error("Can't adjust origin profile: %s", err) + raise AppArmorFileError() + + # Write into new file + try: + with profile_new.open('w') as profile: + profile.writelines(profile_data) + except OSError as err: + _LOGGER.error("Can't write new profile: %s", err) + raise AppArmorFileError() diff --git a/hassio/validate.py b/hassio/validate.py index 480c8592f..61e864343 100644 --- a/hassio/validate.py +++ b/hassio/validate.py @@ -17,7 +17,7 @@ RE_REPOSITORY = re.compile(r"^(?P[^#]+)(?:#(?P[\w\-]+))?$") NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)) WAIT_BOOT = vol.All(vol.Coerce(int), vol.Range(min=1, max=60)) DOCKER_IMAGE = vol.Match(r"^[\w{}]+/[\-\w{}]+$") -ALSA_DEVICE = vol.Any(None, vol.Match(r"\d+,\d+")) +ALSA_DEVICE = vol.Maybe(vol.Match(r"\d+,\d+")) CHANNELS = vol.In([CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV]) @@ -87,7 +87,7 @@ SCHEMA_HASS_CONFIG = vol.Schema({ vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE, vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str), vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT, - vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)), + vol.Optional(ATTR_PASSWORD): vol.Maybe(vol.Coerce(str)), vol.Optional(ATTR_SSL, default=False): vol.Boolean(), vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(), vol.Optional(ATTR_WAIT_BOOT, default=600): diff --git a/setup.py b/setup.py index a9ceff64f..c61f51300 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ setup( install_requires=[ 'attr==0.3.1', 'async_timeout==3.0.0', - 'aiohttp==3.2.1', + 'aiohttp==3.3.2', 'docker==3.3.0', 'colorlog==3.1.2', 'voluptuous==0.11.1',