diff --git a/API.md b/API.md index 86689a9b4..aac11806b 100644 --- a/API.md +++ b/API.md @@ -427,6 +427,8 @@ 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", "icon": "bool", diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index 83cd86d84..b3d6660d8 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -23,7 +23,9 @@ from ..const import ( ATTR_STATE, ATTR_TIMEOUT, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_WEBUI, 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_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES, + ATTR_SECCOMP, 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 @@ -316,6 +318,24 @@ 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.""" + if not self._mesh.get(ATTR_APPARMOR): + return SECURITY_DISABLE + elif self.path_apparmor.exists(): + return SECURITY_PROFILE + return SECURITY_DEFAULT + @property def legacy(self): """Return if the add-on don't support hass labels.""" @@ -474,6 +494,16 @@ 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') + def save_data(self): """Save data of addon.""" self._addons.data.save_data() diff --git a/hassio/addons/build.py b/hassio/addons/build.py index a3dcfb5b3..d98c9597a 100644 --- a/hassio/addons/build.py +++ b/hassio/addons/build.py @@ -55,8 +55,8 @@ class AddonBuild(JsonConfig, CoreSysAttributes): 'io.hass.version': version, 'io.hass.arch': self._arch, 'io.hass.type': META_ADDON, - 'io.hass.name': self.addon.name, - 'io.hass.description': self.addon.description, + 'io.hass.name': self._fix_label('name'), + 'io.hass.description': self._fix_label('description'), }, 'buildargs': { 'BUILD_FROM': self.base_image, @@ -70,3 +70,8 @@ class AddonBuild(JsonConfig, CoreSysAttributes): args['labels']['io.hass.url'] = self.addon.url return args + + def _fix_label(self, label_name): + """Remove characters they are not supported.""" + label = getattr(self.addon, label_name, "") + return label.replace("'", "") diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index 0094b826f..c5ed81d28 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -17,7 +17,8 @@ from ..const import ( ATTR_AUTO_UPDATE, ATTR_WEBUI, ATTR_AUDIO, ATTR_AUDIO_INPUT, ATTR_HOST_IPC, 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_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY, + ATTR_SECCOMP, ATTR_APPARMOR) from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_CHANNEL _LOGGER = logging.getLogger(__name__) @@ -107,6 +108,8 @@ 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(), vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(), diff --git a/hassio/api/__init__.py b/hassio/api/__init__.py index 18f742bb1..c3d878751 100644 --- a/hassio/api/__init__.py +++ b/hassio/api/__init__.py @@ -51,178 +51,163 @@ class RestAPI(CoreSysAttributes): api_host = APIHost() api_host.coresys = self.coresys - self.webapp.router.add_get('/host/info', api_host.info) - self.webapp.router.add_get('/host/hardware', api_host.hardware) - self.webapp.router.add_post('/host/reboot', api_host.reboot) - self.webapp.router.add_post('/host/shutdown', api_host.shutdown) - self.webapp.router.add_post('/host/update', api_host.update) - self.webapp.router.add_post('/host/options', api_host.options) - self.webapp.router.add_post('/host/reload', api_host.reload) + self.webapp.add_routes([ + web.get('/host/info', api_host.info), + web.get('/host/hardware', api_host.hardware), + web.post('/host/reboot', api_host.reboot), + web.post('/host/shutdown', api_host.shutdown), + web.post('/host/update', api_host.update), + web.post('/host/options', api_host.options), + web.post('/host/reload', api_host.reload), + ]) def _register_network(self): """Register network function.""" api_net = APINetwork() api_net.coresys = self.coresys - self.webapp.router.add_get('/network/info', api_net.info) - self.webapp.router.add_post('/network/options', api_net.options) + self.webapp.add_routes([ + web.get('/network/info', api_net.info), + web.post('/network/options', api_net.options), + ]) def _register_supervisor(self): """Register supervisor function.""" api_supervisor = APISupervisor() api_supervisor.coresys = self.coresys - self.webapp.router.add_get('/supervisor/ping', api_supervisor.ping) - self.webapp.router.add_get('/supervisor/info', api_supervisor.info) - self.webapp.router.add_get('/supervisor/stats', api_supervisor.stats) - self.webapp.router.add_post( - '/supervisor/update', api_supervisor.update) - self.webapp.router.add_post( - '/supervisor/reload', api_supervisor.reload) - self.webapp.router.add_post( - '/supervisor/options', api_supervisor.options) - self.webapp.router.add_get('/supervisor/logs', api_supervisor.logs) + self.webapp.add_routes([ + web.get('/supervisor/ping', api_supervisor.ping), + web.get('/supervisor/info', api_supervisor.info), + web.get('/supervisor/stats', api_supervisor.stats), + web.get('/supervisor/logs', api_supervisor.logs), + web.post('/supervisor/update', api_supervisor.update), + web.post('/supervisor/reload', api_supervisor.reload), + web.post('/supervisor/options', api_supervisor.options), + ]) def _register_homeassistant(self): """Register homeassistant function.""" api_hass = APIHomeAssistant() api_hass.coresys = self.coresys - self.webapp.router.add_get('/homeassistant/info', api_hass.info) - self.webapp.router.add_get('/homeassistant/logs', api_hass.logs) - self.webapp.router.add_get('/homeassistant/stats', api_hass.stats) - self.webapp.router.add_post('/homeassistant/options', api_hass.options) - self.webapp.router.add_post('/homeassistant/update', api_hass.update) - self.webapp.router.add_post('/homeassistant/restart', api_hass.restart) - self.webapp.router.add_post('/homeassistant/stop', api_hass.stop) - self.webapp.router.add_post('/homeassistant/start', api_hass.start) - self.webapp.router.add_post('/homeassistant/check', api_hass.check) + self.webapp.add_routes([ + web.get('/homeassistant/info', api_hass.info), + web.get('/homeassistant/logs', api_hass.logs), + web.get('/homeassistant/stats', api_hass.stats), + web.post('/homeassistant/options', api_hass.options), + web.post('/homeassistant/update', api_hass.update), + web.post('/homeassistant/restart', api_hass.restart), + web.post('/homeassistant/stop', api_hass.stop), + web.post('/homeassistant/start', api_hass.start), + web.post('/homeassistant/check', api_hass.check), + ]) def _register_proxy(self): """Register HomeAssistant API Proxy.""" api_proxy = APIProxy() api_proxy.coresys = self.coresys - self.webapp.router.add_get( - '/homeassistant/api/websocket', api_proxy.websocket) - self.webapp.router.add_get( - '/homeassistant/websocket', api_proxy.websocket) - self.webapp.router.add_get( - '/homeassistant/api/stream', api_proxy.stream) - self.webapp.router.add_post( - '/homeassistant/api/{path:.+}', api_proxy.api) - self.webapp.router.add_get( - '/homeassistant/api/{path:.+}', api_proxy.api) - self.webapp.router.add_get( - '/homeassistant/api/', api_proxy.api) + self.webapp.add_routes([ + web.get('/homeassistant/api/websocket', api_proxy.websocket), + web.get('/homeassistant/websocket', api_proxy.websocket), + web.get('/homeassistant/api/stream', api_proxy.stream), + web.post('/homeassistant/api/{path:.+}', api_proxy.api), + web.get('/homeassistant/api/{path:.+}', api_proxy.api), + web.get('/homeassistant/api/', api_proxy.api), + ]) def _register_addons(self): """Register homeassistant function.""" api_addons = APIAddons() api_addons.coresys = self.coresys - self.webapp.router.add_get('/addons', api_addons.list) - self.webapp.router.add_post('/addons/reload', api_addons.reload) - self.webapp.router.add_get('/addons/{addon}/info', api_addons.info) - self.webapp.router.add_post( - '/addons/{addon}/install', api_addons.install) - self.webapp.router.add_post( - '/addons/{addon}/uninstall', api_addons.uninstall) - self.webapp.router.add_post('/addons/{addon}/start', api_addons.start) - self.webapp.router.add_post('/addons/{addon}/stop', api_addons.stop) - self.webapp.router.add_post( - '/addons/{addon}/restart', api_addons.restart) - self.webapp.router.add_post( - '/addons/{addon}/update', api_addons.update) - self.webapp.router.add_post( - '/addons/{addon}/options', api_addons.options) - self.webapp.router.add_post( - '/addons/{addon}/rebuild', api_addons.rebuild) - self.webapp.router.add_get('/addons/{addon}/logs', api_addons.logs) - self.webapp.router.add_get('/addons/{addon}/icon', api_addons.icon) - self.webapp.router.add_get('/addons/{addon}/logo', api_addons.logo) - self.webapp.router.add_get( - '/addons/{addon}/changelog', api_addons.changelog) - self.webapp.router.add_post('/addons/{addon}/stdin', api_addons.stdin) - self.webapp.router.add_get('/addons/{addon}/stats', api_addons.stats) + self.webapp.add_routes([ + web.get('/addons', api_addons.list), + web.post('/addons/reload', api_addons.reload), + web.get('/addons/{addon}/info', api_addons.info), + web.post('/addons/{addon}/install', api_addons.install), + web.post('/addons/{addon}/uninstall', api_addons.uninstall), + web.post('/addons/{addon}/start', api_addons.start), + web.post('/addons/{addon}/stop', api_addons.stop), + web.post('/addons/{addon}/restart', api_addons.restart), + web.post('/addons/{addon}/update', api_addons.update), + web.post('/addons/{addon}/options', api_addons.options), + web.post('/addons/{addon}/rebuild', api_addons.rebuild), + web.get('/addons/{addon}/logs', api_addons.logs), + web.get('/addons/{addon}/icon', api_addons.icon), + web.get('/addons/{addon}/logo', api_addons.logo), + web.get('/addons/{addon}/changelog', api_addons.changelog), + web.post('/addons/{addon}/stdin', api_addons.stdin), + web.get('/addons/{addon}/stats', api_addons.stats), + ]) def _register_snapshots(self): """Register snapshots function.""" api_snapshots = APISnapshots() api_snapshots.coresys = self.coresys - self.webapp.router.add_get('/snapshots', api_snapshots.list) - self.webapp.router.add_post('/snapshots/reload', api_snapshots.reload) - - self.webapp.router.add_post( - '/snapshots/new/full', api_snapshots.snapshot_full) - self.webapp.router.add_post( - '/snapshots/new/partial', api_snapshots.snapshot_partial) - self.webapp.router.add_post( - '/snapshots/new/upload', api_snapshots.upload) - - self.webapp.router.add_get( - '/snapshots/{snapshot}/info', api_snapshots.info) - self.webapp.router.add_post( - '/snapshots/{snapshot}/remove', api_snapshots.remove) - self.webapp.router.add_post( - '/snapshots/{snapshot}/restore/full', api_snapshots.restore_full) - self.webapp.router.add_post( - '/snapshots/{snapshot}/restore/partial', - api_snapshots.restore_partial) - self.webapp.router.add_get( - '/snapshots/{snapshot}/download', - api_snapshots.download) + self.webapp.add_routes([ + web.get('/snapshots', api_snapshots.list), + web.post('/snapshots/reload', api_snapshots.reload), + web.post('/snapshots/new/full', api_snapshots.snapshot_full), + web.post('/snapshots/new/partial', api_snapshots.snapshot_partial), + web.post('/snapshots/new/upload', api_snapshots.upload), + web.get('/snapshots/{snapshot}/info', api_snapshots.info), + web.post('/snapshots/{snapshot}/remove', api_snapshots.remove), + web.post('/snapshots/{snapshot}/restore/full', + api_snapshots.restore_full), + web.post('/snapshots/{snapshot}/restore/partial', + api_snapshots.restore_partial), + web.get('/snapshots/{snapshot}/download', api_snapshots.download), + ]) def _register_services(self): api_services = APIServices() api_services.coresys = self.coresys - self.webapp.router.add_get('/services', api_services.list) - - self.webapp.router.add_get( - '/services/{service}', api_services.get_service) - self.webapp.router.add_post( - '/services/{service}', api_services.set_service) - self.webapp.router.add_delete( - '/services/{service}', api_services.del_service) + self.webapp.add_routes([ + web.get('/services', api_services.list), + web.get('/services/{service}', api_services.get_service), + web.post('/services/{service}', api_services.set_service), + web.delete('/services/{service}', api_services.del_service), + ]) def _register_discovery(self): api_discovery = APIDiscovery() api_discovery.coresys = self.coresys - self.webapp.router.add_get( - '/services/discovery', api_discovery.list) - self.webapp.router.add_get( - '/services/discovery/{uuid}', api_discovery.get_discovery) - self.webapp.router.add_delete( - '/services/discovery/{uuid}', api_discovery.del_discovery) - self.webapp.router.add_post( - '/services/discovery', api_discovery.set_discovery) + self.webapp.add_routes([ + web.get('/services/discovery', api_discovery.list), + web.get('/services/discovery/{uuid}', api_discovery.get_discovery), + web.delete('/services/discovery/{uuid}', + api_discovery.del_discovery), + web.post('/services/discovery', api_discovery.set_discovery), + ]) def _register_panel(self): """Register panel for homeassistant.""" - def create_panel_response(build_type): + def create_response(build_type): """Create a function to generate a response.""" path = Path(__file__).parent.joinpath( f"panel/{build_type}.html") return lambda request: web.FileResponse(path) # This route is for backwards compatibility with HA < 0.58 - self.webapp.router.add_get( - '/panel', create_panel_response('hassio-main-es5')) + self.webapp.add_routes([ + web.get('/panel', create_response('hassio-main-es5'))]) # This route is for backwards compatibility with HA 0.58 - 0.61 - self.webapp.router.add_get( - '/panel_es5', create_panel_response('hassio-main-es5')) - self.webapp.router.add_get( - '/panel_latest', create_panel_response('hassio-main-latest')) + self.webapp.add_routes([ + web.get('/panel_es5', create_response('hassio-main-es5')), + web.get('/panel_latest', create_response('hassio-main-latest')), + ]) # This route is for HA > 0.61 - self.webapp.router.add_get( - '/app-es5/index.html', create_panel_response('index')) - self.webapp.router.add_get( - '/app-es5/hassio-app.html', create_panel_response('hassio-app')) + self.webapp.add_routes([ + web.get('/app-es5/index.html', create_response('index')), + web.get('/app-es5/hassio-app.html', create_response('hassio-app')), + ]) async def start(self): """Run rest api webserver.""" diff --git a/hassio/api/addons.py b/hassio/api/addons.py index b1909cbd3..bb00750e2 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_DISCOVERY, ATTR_SECCOMP, ATTR_APPARMOR, CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT) from ..coresys import CoreSysAttributes from ..validate import DOCKER_PORTS @@ -123,6 +123,8 @@ 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, ATTR_LOGO: addon.with_logo, diff --git a/hassio/const.py b/hassio/const.py index b0935e4cf..f91aa2538 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -2,7 +2,7 @@ from pathlib import Path from ipaddress import ip_network -HASSIO_VERSION = '0.99' +HASSIO_VERSION = '1.0' URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/' 'hassio/{}/version.json') @@ -159,6 +159,8 @@ ATTR_DISCOVERY = 'discovery' ATTR_PROTECTED = 'protected' ATTR_CRYPTO = 'crypto' ATTR_BRANCH = 'branch' +ATTR_SECCOMP = 'seccomp' +ATTR_APPARMOR = 'apparmor' SERVICE_MQTT = 'mqtt' @@ -202,3 +204,7 @@ SNAPSHOT_FULL = 'full' SNAPSHOT_PARTIAL = 'partial' CRYPTO_AES128 = 'aes128' + +SECURITY_PROFILE = 'profile' +SECURITY_DEFAULT = 'default' +SECURITY_DISABLE = 'disable' diff --git a/hassio/docker/addon.py b/hassio/docker/addon.py index e60a99202..e766be5e0 100644 --- a/hassio/docker/addon.py +++ b/hassio/docker/addon.py @@ -9,7 +9,7 @@ from .interface import DockerInterface from ..addons.build import AddonBuild from ..const import ( MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP, MAP_SHARE, ENV_TOKEN, - ENV_TIME) + ENV_TIME, SECURITY_PROFILE, SECURITY_DISABLE) from ..utils import process_lock _LOGGER = logging.getLogger(__name__) @@ -121,14 +121,21 @@ class DockerAddon(DockerInterface): @property def security_opt(self): """Controlling security opt.""" - privileged = self.addon.privileged or [] + security = [] - # Disable AppArmor sinse it make troubles wit SYS_ADMIN - if 'SYS_ADMIN' in privileged: - return [ - "apparmor:unconfined", - ] - return None + # AppArmor + if 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}") + + return security or None @property def tmpfs(self): diff --git a/hassio/docker/interface.py b/hassio/docker/interface.py index 69e69052b..f14ddae46 100644 --- a/hassio/docker/interface.py +++ b/hassio/docker/interface.py @@ -264,31 +264,6 @@ class DockerInterface(CoreSysAttributes): except docker.errors.DockerException as err: _LOGGER.warning("Can't grap logs from %s: %s", self.image, err) - @process_lock - def restart(self): - """Restart docker container.""" - return self._loop.run_in_executor(None, self._restart) - - def _restart(self): - """Restart docker container. - - Need run inside executor. - """ - try: - container = self._docker.containers.get(self.name) - except docker.errors.DockerException: - return False - - _LOGGER.info("Restart %s", self.image) - - try: - container.restart(timeout=self.timeout) - except docker.errors.DockerException as err: - _LOGGER.warning("Can't restart %s: %s", self.image, err) - return False - - return True - @process_lock def cleanup(self): """Check if old version exists and cleanup.""" diff --git a/hassio/homeassistant.py b/hassio/homeassistant.py index 57c6af89d..21916cfdc 100644 --- a/hassio/homeassistant.py +++ b/hassio/homeassistant.py @@ -216,17 +216,19 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): async def _start(self): """Start HomeAssistant docker & wait.""" - if await self.instance.run(): - await self._block_till_run() - - @process_lock - async def start(self): - """Run HomeAssistant docker.""" if not await self.instance.run(): return False - return await self._block_till_run() + @process_lock + def start(self): + """Run HomeAssistant docker. + + Return a coroutine. + """ + return self._start() + + @process_lock def stop(self): """Stop HomeAssistant docker. @@ -237,10 +239,8 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): @process_lock async def restart(self): """Restart HomeAssistant docker.""" - if not await self.instance.restart(): - return False - - return await self._block_till_run() + await self.instance.stop() + return await self._start() def logs(self): """Get HomeAssistant docker logs. diff --git a/setup.py b/setup.py index 839793112..3380d639e 100644 --- a/setup.py +++ b/setup.py @@ -40,9 +40,9 @@ setup( ], include_package_data=True, install_requires=[ - 'async_timeout==2.0.0', - 'aiohttp==3.0.9', - 'docker==3.1.1', + 'async_timeout==2.0.1', + 'aiohttp==3.1.2', + 'docker==3.2.0', 'colorlog==3.1.2', 'voluptuous==0.11.1', 'gitpython==2.1.8', diff --git a/version.json b/version.json index 1a6c1f74d..c5d68761b 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "hassio": "0.99", + "hassio": "1.0", "homeassistant": "0.67.0b0", "resinos": "1.3", "resinhup": "0.3",