diff --git a/API.md b/API.md index 69736bb3f..794b99f09 100644 --- a/API.md +++ b/API.md @@ -108,6 +108,40 @@ Reload addons/version. Output the raw docker log +### Security + +- GET `/security/info` +```json +{ + "initialize": "bool", + "totp": "bool" +} +``` + +- POST `/security/options` +```json +{ + "password": "xy" +} +``` + +- POST `/security/totp` +```json +{ + "password": "xy" +} +``` + +Return QR-Code + +- POST `/security/session` +```json +{ + "password": "xy", + "totp": "null|123456" +} +``` + ### Host - POST `/host/shutdown` diff --git a/hassio/addons/data.py b/hassio/addons/data.py index 11589831e..4627434b2 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -14,7 +14,7 @@ from ..const import ( FILE_HASSIO_ADDONS, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_STARTUP, ATTR_BOOT, ATTR_MAP, ATTR_OPTIONS, ATTR_PORTS, BOOT_AUTO, DOCKER_REPO, ATTR_SCHEMA, ATTR_IMAGE, MAP_CONFIG, MAP_SSL, MAP_ADDONS, - MAP_BACKUP, ATTR_REPOSITORY, ATTR_URL, ATTR_ARCH) + MAP_BACKUP, ATTR_REPOSITORY, ATTR_URL, ATTR_ARCH, ATTR_LOCATON) from ..config import Config from ..tools import read_json_file, write_json_file @@ -109,6 +109,7 @@ class AddonsData(Config): # store addon_config[ATTR_REPOSITORY] = repository + addon_config[ATTR_LOCATON] = str(addon.parent) self._addons_cache[addon_slug] = addon_config except OSError: @@ -303,13 +304,31 @@ class AddonsData(Config): def get_image(self, addon): """Return image name of addon.""" addon_data = self._system_data.get( - addon, self._addons_cache.get(addon)) + addon, self._addons_cache.get(addon) + ) - if ATTR_IMAGE not in addon_data: + # core repository + if addon_data[ATTR_REPOSITORY] == REPOSITORY_CORE: return "{}/{}-addon-{}".format( DOCKER_REPO, self.arch, addon_data[ATTR_SLUG]) - return addon_data[ATTR_IMAGE].format(arch=self.arch) + # Repository with dockerhub images + if ATTR_IMAGE in addon_data: + return addon_data[ATTR_IMAGE].format(arch=self.arch) + + # Local build addon + if addon_data[ATTR_REPOSITORY] == REPOSITORY_LOCAL: + return "local/{}-addon-{}".format(self.arch, addon_data[ATTR_SLUG]) + + _LOGGER.error("No image for %s", addon) + + def need_build(self, addon): + """Return True if this addon need a local build.""" + addon_data = self._system_data.get( + addon, self._addons_cache.get(addon) + ) + return addon_data[ATTR_REPOSITORY] == REPOSITORY_LOCAL \ + and not addon_data.get(ATTR_IMAGE) def map_config(self, addon): """Return True if config map is needed.""" @@ -333,12 +352,16 @@ class AddonsData(Config): def path_extern_data(self, addon): """Return addon data path external for docker.""" - return str(PurePath(self.config.path_extern_addons_data, addon)) + return PurePath(self.config.path_extern_addons_data, addon) def path_addon_options(self, addon): """Return path to addons options.""" return Path(self.path_data(addon), "options.json") + def path_addon_location(self, addon): + """Return path to this addon.""" + return Path(self._addons_cache[addon][ATTR_LOCATON]) + def write_addon_options(self, addon): """Return True if addon options is written to data.""" schema = self.get_schema(addon) diff --git a/hassio/api/__init__.py b/hassio/api/__init__.py index caca6d2f0..c037ee53c 100644 --- a/hassio/api/__init__.py +++ b/hassio/api/__init__.py @@ -8,6 +8,7 @@ from .homeassistant import APIHomeAssistant from .host import APIHost from .network import APINetwork from .supervisor import APISupervisor +from .security import APISecurity _LOGGER = logging.getLogger(__name__) @@ -86,6 +87,15 @@ class RestAPI(object): '/addons/{addon}/options', api_addons.options) self.webapp.router.add_get('/addons/{addon}/logs', api_addons.logs) + def register_security(self): + """Register security function.""" + api_security = APISecurity(self.config, self.loop) + + self.webapp.router.add_get('/security/info', api_security.info) + self.webapp.router.add_post('/security/options', api_security.options) + self.webapp.router.add_post('/security/totp', api_security.totp) + self.webapp.router.add_post('/security/session', api_security.session) + async def start(self): """Run rest api webserver.""" self._handler = self.webapp.make_handler(loop=self.loop) diff --git a/hassio/api/homeassistant.py b/hassio/api/homeassistant.py index 35c8f67d5..8410fe7d5 100644 --- a/hassio/api/homeassistant.py +++ b/hassio/api/homeassistant.py @@ -26,13 +26,11 @@ class APIHomeAssistant(object): @api_process async def info(self, request): """Return host information.""" - info = { + return { ATTR_VERSION: self.homeassistant.version, ATTR_LAST_VERSION: self.config.last_homeassistant, } - return info - @api_process async def update(self, request): """Update homeassistant.""" diff --git a/hassio/api/security.py b/hassio/api/security.py new file mode 100644 index 000000000..12a09f638 --- /dev/null +++ b/hassio/api/security.py @@ -0,0 +1,102 @@ +"""Init file for HassIO security rest api.""" +from datetime import datetime, timedelta +import io +import logging +import hashlib +import os + +from aiohttp import web +import voluptuous as vol +import pyotp +import pyqrcode + +from .util import api_process, api_validate, hash_password +from ..const import ATTR_INITIALIZE, ATTR_PASSWORD, ATTR_TOTP, ATTR_SESSION + +_LOGGER = logging.getLogger(__name__) + +SCHEMA_PASSWORD = vol.Schema({ + vol.Required(ATTR_PASSWORD): vol.Coerce(str), +}) + +SCHEMA_SESSION = SCHEMA_PASSWORD.extend({ + vol.Optional(ATTR_TOTP, default=None): vol.Coerce(str), +}) + + +class APISecurity(object): + """Handle rest api for security functions.""" + + def __init__(self, config, loop): + """Initialize security rest api part.""" + self.config = config + self.loop = loop + + def _check_password(self, body): + """Check if password is valid and security is initialize.""" + if not self.config.security_initialize: + raise RuntimeError("First set a password") + + password = hash_password(body[ATTR_PASSWORD]) + if password != self.config.security_password: + raise RuntimeError("Wrong password") + + @api_process + async def info(self, request): + """Return host information.""" + return { + ATTR_INITIALIZE: self.config.security_initialize, + ATTR_TOTP: self.config.security_totp is not None, + } + + @api_process + async def options(self, request): + """Set options / password.""" + body = await api_validate(SCHEMA_PASSWORD, request) + + if self.config.security_initialize: + raise RuntimeError("Password is already set!") + + self.config.security_password = hash_password(body[ATTR_PASSWORD]) + self.config.security_initialize = True + return True + + @api_process + async def totp(self, request): + """Set and initialze TOTP.""" + body = await api_validate(SCHEMA_PASSWORD, request) + self._check_password(body) + + # generate TOTP + totp_init_key = pyotp.random_base32() + totp = pyotp.TOTP(totp_init_key) + + # init qrcode + buff = io.BytesIO() + + qrcode = pyqrcode.create(totp.provisioning_uri("Hass.IO")) + qrcode.svg(buff) + + # finish + self.config.security_totp = totp_init_key + return web.Response(body=buff.getvalue(), content_type='image/svg+xml') + + @api_process + async def session(self, request): + """Set and initialze session.""" + body = await api_validate(SCHEMA_SESSION, request) + self._check_password(body) + + # check TOTP + if self.config.security_totp: + totp = pyotp.TOTP(self.config.security_totp) + if body[ATTR_TOTP] != totp.now(): + raise RuntimeError("Invalid TOTP token!") + + # create session + valid_until = datetime.now() + timedelta(days=1) + session = hashlib.sha256(os.urandom(54)).hexdigest() + + # store session + self.config.security_sessions = (session, valid_until) + return {ATTR_SESSION: session} diff --git a/hassio/api/util.py b/hassio/api/util.py index 5204e9079..b59352dea 100644 --- a/hassio/api/util.py +++ b/hassio/api/util.py @@ -1,5 +1,6 @@ """Init file for HassIO util for rest api.""" import json +import hashlib import logging from aiohttp import web @@ -32,6 +33,8 @@ def api_process(method): if isinstance(answer, dict): return api_return_ok(data=answer) + if isinstance(answer, web.Response): + return answer elif answer: return api_return_ok() return api_return_error() @@ -101,3 +104,9 @@ async def api_validate(schema, request): raise RuntimeError(humanize_error(data, ex)) from None return data + + +def hash_password(password): + """Hash and salt our passwords.""" + key = ")*()*SALT_HASSIO2123{}6554547485HSKA!!*JSLAfdasda$".format(password) + return hashlib.sha256(key.encode()).hexdigest() diff --git a/hassio/bootstrap.py b/hassio/bootstrap.py index b55b63f59..89a37cdcf 100644 --- a/hassio/bootstrap.py +++ b/hassio/bootstrap.py @@ -42,6 +42,11 @@ def initialize_system_data(websession): config.path_addons_git) config.path_addons_git.mkdir(parents=True) + if not config.path_addons_build.is_dir(): + _LOGGER.info("Create Home-Assistant addon build folder %s", + config.path_addons_build) + config.path_addons_build.mkdir(parents=True) + # homeassistant backup folder if not config.path_backup.is_dir(): _LOGGER.info("Create Home-Assistant backup folder %s", diff --git a/hassio/config.py b/hassio/config.py index 8965ced69..fad13e98a 100644 --- a/hassio/config.py +++ b/hassio/config.py @@ -1,4 +1,5 @@ """Bootstrap HassIO.""" +from datetime import datetime import logging import json import os @@ -13,6 +14,8 @@ from .tools import ( _LOGGER = logging.getLogger(__name__) +DATETIME_FORMAT = "%Y%m%d %H:%M:%S" + HOMEASSISTANT_CONFIG = PurePath("homeassistant") HOMEASSISTANT_LAST = 'homeassistant_last' @@ -24,6 +27,7 @@ ADDONS_CORE = PurePath("addons/core") ADDONS_LOCAL = PurePath("addons/local") ADDONS_GIT = PurePath("addons/git") ADDONS_DATA = PurePath("addons/data") +ADDONS_BUILD = PurePath("addons/build") ADDONS_CUSTOM_LIST = 'addons_custom_list' BACKUP_DATA = PurePath("backup") @@ -32,6 +36,11 @@ UPSTREAM_BETA = 'upstream_beta' API_ENDPOINT = 'api_endpoint' +SECURITY_INITIALIZE = 'security_initialize' +SECURITY_TOTP = 'security_totp' +SECURITY_PASSWORD = 'security_password' +SECURITY_SESSIONS = 'security_sessions' + # pylint: disable=no-value-for-parameter SCHEMA_CONFIG = vol.Schema({ @@ -41,6 +50,11 @@ SCHEMA_CONFIG = vol.Schema({ vol.Optional(HASSIO_LAST): vol.Coerce(str), vol.Optional(HASSIO_CLEANUP): vol.Coerce(str), vol.Optional(ADDONS_CUSTOM_LIST, default=[]): [vol.Url()], + vol.Optional(SECURITY_INITIALIZE, default=False): vol.Boolean(), + vol.Optional(SECURITY_TOTP): vol.Coerce(str), + vol.Optional(SECURITY_PASSWORD): vol.Coerce(str), + vol.Optional(SECURITY_SESSIONS, default={}): + {vol.Coerce(str): vol.Coerce(str)}, }, extra=vol.REMOVE_EXTRA) @@ -192,7 +206,7 @@ class CoreConfig(Config): @property def path_extern_addons_local(self): """Return path for customs addons.""" - return str(PurePath(self.path_extern_hassio, ADDONS_LOCAL)) + return PurePath(self.path_extern_hassio, ADDONS_LOCAL) @property def path_addons_data(self): @@ -202,7 +216,12 @@ class CoreConfig(Config): @property def path_extern_addons_data(self): """Return root addon data folder extern for docker.""" - return str(PurePath(self.path_extern_hassio, ADDONS_DATA)) + return PurePath(self.path_extern_hassio, ADDONS_DATA) + + @property + def path_addons_build(self): + """Return root addon build folder.""" + return Path(HASSIO_SHARE, ADDONS_BUILD) @property def path_backup(self): @@ -212,7 +231,7 @@ class CoreConfig(Config): @property def path_extern_backup(self): """Return root backup data folder extern for docker.""" - return str(PurePath(self.path_extern_hassio, BACKUP_DATA)) + return PurePath(self.path_extern_hassio, BACKUP_DATA) @property def addons_repositories(self): @@ -235,3 +254,55 @@ class CoreConfig(Config): self._data[ADDONS_CUSTOM_LIST].remove(repo) self.save() + + @property + def security_initialize(self): + """Return is security was initialize.""" + return self._data[SECURITY_INITIALIZE] + + @security_initialize.setter + def security_initialize(self, value): + """Set is security initialize.""" + self._data[SECURITY_INITIALIZE] = value + self.save() + + @property + def security_totp(self): + """Return the TOTP key.""" + return self._data.get(SECURITY_TOTP) + + @security_totp.setter + def security_totp(self, value): + """Set the TOTP key.""" + self._data[SECURITY_TOTP] = value + self.save() + + @property + def security_password(self): + """Return the password key.""" + return self._data.get(SECURITY_PASSWORD) + + @security_password.setter + def security_password(self, value): + """Set the password key.""" + self._data[SECURITY_PASSWORD] = value + self.save() + + @property + def security_sessions(self): + """Return api sessions.""" + return {session: datetime.strptime(until, DATETIME_FORMAT) for + session, until in self._data[SECURITY_SESSIONS].items()} + + @security_sessions.setter + def security_sessions(self, value): + """Set the a new session.""" + session, valid = value + if valid is None: + self._data[SECURITY_SESSIONS].pop(session, None) + else: + self._data[SECURITY_SESSIONS].update( + {session: valid.strftime(DATETIME_FORMAT)} + ) + + self.save() diff --git a/hassio/const.py b/hassio/const.py index acdd79fcb..d7df6ffc4 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -1,7 +1,7 @@ """Const file for HassIO.""" from pathlib import Path -HASSIO_VERSION = '0.23' +HASSIO_VERSION = '0.24' URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/' 'hassio/master/version.json') @@ -18,6 +18,7 @@ RUN_UPDATE_INFO_TASKS = 28800 RUN_UPDATE_SUPERVISOR_TASKS = 29100 RUN_RELOAD_ADDONS_TASKS = 28800 RUN_WATCHDOG_HOMEASSISTANT = 15 +RUN_CLEANUP_API_SESSIONS = 900 RESTART_EXIT_CODE = 100 @@ -62,6 +63,11 @@ ATTR_REPOSITORY = 'repository' ATTR_REPOSITORIES = 'repositories' ATTR_URL = 'url' ATTR_MAINTAINER = 'maintainer' +ATTR_PASSWORD = 'password' +ATTR_TOTP = 'totp' +ATTR_INITIALIZE = 'initialize' +ATTR_SESSION = 'session' +ATTR_LOCATON = 'location' STARTUP_BEFORE = 'before' STARTUP_AFTER = 'after' diff --git a/hassio/core.py b/hassio/core.py index 8f2c59830..cbcb4cc46 100644 --- a/hassio/core.py +++ b/hassio/core.py @@ -11,12 +11,14 @@ from .api import RestAPI from .host_control import HostControl from .const import ( SOCKET_DOCKER, RUN_UPDATE_INFO_TASKS, RUN_RELOAD_ADDONS_TASKS, - RUN_UPDATE_SUPERVISOR_TASKS, RUN_WATCHDOG_HOMEASSISTANT, STARTUP_AFTER, - STARTUP_BEFORE) + RUN_UPDATE_SUPERVISOR_TASKS, RUN_WATCHDOG_HOMEASSISTANT, + RUN_CLEANUP_API_SESSIONS, STARTUP_AFTER, STARTUP_BEFORE) from .scheduler import Scheduler from .dock.homeassistant import DockerHomeAssistant from .dock.supervisor import DockerSupervisor -from .tasks import hassio_update, homeassistant_watchdog, homeassistant_setup +from .tasks import ( + hassio_update, homeassistant_watchdog, homeassistant_setup, + api_sessions_cleanup) from .tools import get_arch_from_image, get_local_ip _LOGGER = logging.getLogger(__name__) @@ -71,6 +73,12 @@ class HassIO(object): self.supervisor, self.addons, self.host_control) self.api.register_homeassistant(self.homeassistant) self.api.register_addons(self.addons) + self.api.register_security() + + # schedule api session cleanup + self.scheduler.register_task( + api_sessions_cleanup(self.config), RUN_CLEANUP_API_SESSIONS, + now=True) # schedule update info tasks self.scheduler.register_task( @@ -102,24 +110,26 @@ class HassIO(object): await self.api.start() _LOGGER.info("Start hassio api on %s", self.config.api_endpoint) - # HomeAssistant is already running / supervisor have only reboot - if await self.homeassistant.is_running(): - _LOGGER.info("HassIO reboot detected") - return + try: + # HomeAssistant is already running / supervisor have only reboot + if await self.homeassistant.is_running(): + _LOGGER.info("HassIO reboot detected") + return - # start addon mark as before - await self.addons.auto_boot(STARTUP_BEFORE) + # start addon mark as before + await self.addons.auto_boot(STARTUP_BEFORE) - # run HomeAssistant - await self.homeassistant.run() + # run HomeAssistant + await self.homeassistant.run() - # schedule homeassistant watchdog - self.scheduler.register_task( - homeassistant_watchdog(self.loop, self.homeassistant), - RUN_WATCHDOG_HOMEASSISTANT) + # start addon mark as after + await self.addons.auto_boot(STARTUP_AFTER) - # start addon mark as after - await self.addons.auto_boot(STARTUP_AFTER) + finally: + # schedule homeassistant watchdog + self.scheduler.register_task( + homeassistant_watchdog(self.loop, self.homeassistant), + RUN_WATCHDOG_HOMEASSISTANT) async def stop(self, exit_code=0): """Stop a running orchestration.""" diff --git a/hassio/dock/addon.py b/hassio/dock/addon.py index 1c3399bc6..88ace43d7 100644 --- a/hassio/dock/addon.py +++ b/hassio/dock/addon.py @@ -1,15 +1,16 @@ """Init file for HassIO addon docker object.""" import logging +from pathlib import Path +import shutil import docker from . import DockerBase +from .util import dockerfile_template from ..tools import get_version_from_env _LOGGER = logging.getLogger(__name__) -HASS_DOCKER_NAME = 'homeassistant' - class DockerAddon(DockerBase): """Docker hassio wrapper for HomeAssistant.""" @@ -30,31 +31,31 @@ class DockerAddon(DockerBase): def volumes(self): """Generate volumes for mappings.""" volumes = { - self.addons_data.path_extern_data(self.addon): { + str(self.addons_data.path_extern_data(self.addon)): { 'bind': '/data', 'mode': 'rw' }} if self.addons_data.map_config(self.addon): volumes.update({ - self.config.path_extern_config: { + str(self.config.path_extern_config): { 'bind': '/config', 'mode': 'rw' }}) if self.addons_data.map_ssl(self.addon): volumes.update({ - self.config.path_extern_ssl: { + str(self.config.path_extern_ssl): { 'bind': '/ssl', 'mode': 'rw' }}) if self.addons_data.map_addons(self.addon): volumes.update({ - self.config.path_extern_addons_local: { + str(self.config.path_extern_addons_local): { 'bind': '/addons', 'mode': 'rw' }}) if self.addons_data.map_backup(self.addon): volumes.update({ - self.config.path_extern_backup: { + str(self.config.path_extern_backup): { 'bind': '/backup', 'mode': 'rw' }}) @@ -102,7 +103,69 @@ class DockerAddon(DockerBase): self.container = self.dock.containers.get(self.docker_name) self.version = get_version_from_env( self.container.attrs['Config']['Env']) - _LOGGER.info("Attach to image %s with version %s", - self.image, self.version) + + _LOGGER.info( + "Attach to image %s with version %s", self.image, self.version) except (docker.errors.DockerException, KeyError): pass + + def _install(self, tag): + """Pull docker image or build it. + + Need run inside executor. + """ + if self.addons_data.need_build(self.addon): + return self._build(tag) + + return super()._install(tag) + + async def build(self, tag): + """Build a docker container.""" + if self._lock.locked(): + _LOGGER.error("Can't excute build while a task is in progress") + return False + + async with self._lock: + return await self.loop.run_in_executor(None, self._build, tag) + + def _build(self, tag): + """Build a docker container. + + Need run inside executor. + """ + build_dir = Path(self.config.path_addons_build, self.addon) + try: + # prepare temporary addon build folder + try: + source = self.addons_data.path_addon_location(self.addon) + shutil.copytree(str(source), str(build_dir)) + except shutil.Error as err: + _LOGGER.error("Can't copy %s to temporary build folder -> %s", + source, build_dir) + return False + + # prepare Dockerfile + try: + dockerfile_template( + Path(build_dir, 'Dockerfile'), self.addons_data.arch, tag) + except OSError as err: + _LOGGER.error("Can't prepare dockerfile -> %s", err) + + # run docker build + try: + build_tag = "{}:{}".format(self.image, tag) + + _LOGGER.info("Start build %s on %s", build_tag, build_dir) + image = self.dock.images.build( + path=str(build_dir), tag=build_tag, pull=True) + + _LOGGER.info("Build %s done", build_tag) + image.tag(self.image, tag='latest') + except (docker.errors.DockerException, TypeError) as err: + _LOGGER.error("Can't build %s -> %s", build_tag, err) + return False + + return True + + finally: + shutil.rmtree(str(build_dir), ignore_errors=True) diff --git a/hassio/dock/homeassistant.py b/hassio/dock/homeassistant.py index 0f314bb2d..f33ea5124 100644 --- a/hassio/dock/homeassistant.py +++ b/hassio/dock/homeassistant.py @@ -45,9 +45,9 @@ class DockerHomeAssistant(DockerBase): 'HASSIO': self.config.api_endpoint, }, volumes={ - self.config.path_extern_config: + str(self.config.path_extern_config): {'bind': '/config', 'mode': 'rw'}, - self.config.path_extern_ssl: + str(self.config.path_extern_ssl): {'bind': '/ssl', 'mode': 'rw'}, }) diff --git a/hassio/dock/util.py b/hassio/dock/util.py new file mode 100644 index 000000000..b851e9b6f --- /dev/null +++ b/hassio/dock/util.py @@ -0,0 +1,32 @@ +"""HassIO docker utilitys.""" +import re + +from ..const import ARCH_AARCH64, ARCH_ARMHF, ARCH_I386, ARCH_AMD64 + + +RESIN_BASE_IMAGE = { + ARCH_ARMHF: "resin/armhf-alpine:3.5", + ARCH_AARCH64: "resin/aarch64-alpine:3.5", + ARCH_I386: "resin/i386-alpine:3.5", + ARCH_AMD64: "resin/amd64-alpine:3.5", +} + +TMPL_VERSION = re.compile(r"%%VERSION%%") +TMPL_IMAGE = re.compile(r"%%BASE_IMAGE%%") + + +def dockerfile_template(dockerfile, arch, version): + """Prepare a Hass.IO dockerfile.""" + buff = [] + resin_image = RESIN_BASE_IMAGE[arch] + + # read docker + with dockerfile.open('r') as dock_input: + for line in dock_input: + line = TMPL_VERSION.sub(version, line) + line = TMPL_IMAGE.sub(resin_image, line) + buff.append(line) + + # write docker + with dockerfile.open('w') as dock_output: + dock_output.writelines(buff) diff --git a/hassio/tasks.py b/hassio/tasks.py index bc55eae99..32e4c8a6d 100644 --- a/hassio/tasks.py +++ b/hassio/tasks.py @@ -1,10 +1,23 @@ """Multible tasks.""" import asyncio +from datetime import datetime import logging _LOGGER = logging.getLogger(__name__) +def api_sessions_cleanup(config): + """Create scheduler task for cleanup api sessions.""" + async def _api_sessions_cleanup(): + """Cleanup old api sessions.""" + now = datetime.now() + for session, until_valid in config.security_sessions.items(): + if now >= until_valid: + config.security_sessions = (session, None) + + return _api_sessions_cleanup + + def hassio_update(config, supervisor): """Create scheduler task for update of supervisor hassio.""" async def _hassio_update(): diff --git a/misc/security.png b/misc/security.png new file mode 100644 index 000000000..8343b0e84 Binary files /dev/null and b/misc/security.png differ diff --git a/misc/security.xml b/misc/security.xml new file mode 100644 index 000000000..162e082f7 --- /dev/null +++ b/misc/security.xml @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/setup.py b/setup.py index 31f333ffe..14374be3a 100644 --- a/setup.py +++ b/setup.py @@ -38,5 +38,7 @@ setup( 'colorlog', 'voluptuous', 'gitpython', + 'pyotp', + 'pyqrcode' ] ) diff --git a/version.json b/version.json index 13299eb24..fbbe3e5d6 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "hassio": "0.23", + "hassio": "0.24", "homeassistant": "0.44.2", "resinos": "0.7", "resinhup": "0.1",