Merge pull request #1979 from home-assistant/dev

Release 236
This commit is contained in:
Pascal Vizeli 2020-08-27 10:52:18 +02:00 committed by GitHub
commit cea6e7a9f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
223 changed files with 8364 additions and 6078 deletions

1
.github/stale.yml vendored
View File

@ -6,6 +6,7 @@ daysUntilClose: 7
exemptLabels:
- pinned
- security
- rfc
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable

View File

@ -12,7 +12,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v2
- name: Sentry Release
uses: getsentry/action-release@v1.0.0
uses: getsentry/action-release@v1.0.1
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/psf/black
rev: 19.10b0
rev: 20.8b1
hooks:
- id: black
args:

65
API.md
View File

@ -442,6 +442,65 @@ Proxy to Home Assistant Core websocket.
}
```
### Network
Network operations over the API
#### GET `/network/info`
Get network information
```json
{
"interfaces": {
"enp0s31f6": {
"ip_address": "192.168.2.148/24",
"gateway": "192.168.2.1",
"id": "Wired connection 1",
"type": "802-3-ethernet",
"nameservers": ["192.168.2.1"],
"method": "static",
"primary": true
}
}
}
```
#### GET `/network/{interface}/info`
Get information for a single interface
```json
{
"ip_address": "192.168.2.148/24",
"gateway": "192.168.2.1",
"id": "Wired connection 1",
"type": "802-3-ethernet",
"nameservers": ["192.168.2.1"],
"method": "dhcp",
"primary": true
}
```
#### POST `/network/{interface}/update`
Update information for a single interface
**Options:**
| Option | Description |
| --------- | ---------------------------------------------------------------------- |
| `address` | The new IP address for the interface in the X.X.X.X/XX format |
| `dns` | Comma seperated list of DNS servers to use |
| `gateway` | The gateway the interface should use |
| `method` | Set if the interface should use DHCP or not, can be `dhcp` or `static` |
_All options are optional._
**NB!: If you change the `address` or `gateway` you may need to reconnect to the new address**
The result will be a updated object.
### RESTful for API add-ons
If an add-on will call itself, you can use `/addons/self/...`.
@ -550,7 +609,8 @@ Get all available add-ons.
"ingress_entry": "null|/api/hassio_ingress/slug",
"ingress_url": "null|/api/hassio_ingress/slug/entry.html",
"ingress_port": "null|int",
"ingress_panel": "null|bool"
"ingress_panel": "null|bool",
"watchdog": "null|bool"
}
```
@ -570,7 +630,8 @@ Get all available add-ons.
"options": {},
"audio_output": "null|0,0",
"audio_input": "null|0,0",
"ingress_panel": "bool"
"ingress_panel": "bool",
"watchdog": "bool"
}
```

View File

@ -14,8 +14,7 @@ RUN \
libffi \
libpulse \
musl \
openssl \
socat
openssl
ARG BUILD_ARCH
WORKDIR /usr/src

@ -1 +1 @@
Subproject commit 77b25f5132820c0596ccae82dd501ce67f101e72
Subproject commit c1a4b27bc7fc68dfb4af6b382238c010340e7912

View File

@ -1,12 +1,12 @@
aiohttp==3.6.2
async_timeout==3.0.1
attrs==19.3.0
attrs==20.1.0
cchardet==2.1.6
colorlog==4.2.1
cpe==1.2.1
cryptography==3.0
debugpy==1.0.0rc1
docker==4.3.0
cryptography==3.1
debugpy==1.0.0rc2
docker==4.3.1
gitpython==3.1.7
jinja2==2.11.2
packaging==20.4
@ -14,6 +14,6 @@ pulsectl==20.5.1
pytz==2020.1
pyudev==0.22.0
ruamel.yaml==0.15.100
sentry-sdk==0.16.5
sentry-sdk==0.17.0
uvloop==0.14.0
voluptuous==0.11.7

View File

@ -1,12 +1,13 @@
black==19.10b0
codecov==2.1.8
black==20.8b1
codecov==2.1.9
coverage==5.2.1
flake8-docstrings==1.5.0
flake8==3.8.3
pre-commit==2.6.0
pydocstyle==5.0.2
pylint==2.5.3
pre-commit==2.7.1
pydocstyle==5.1.0
pylint==2.6.0
pytest-aiohttp==0.3.0
pytest-asyncio==0.12.0 # NB!: Versions over 0.12.0 breaks pytest-aiohttp (https://github.com/aio-libs/pytest-aiohttp/issues/16)
pytest-cov==2.10.1
pytest-timeout==1.4.2
pytest==6.0.1

View File

@ -35,10 +35,18 @@ setup(
"supervisor.docker",
"supervisor.addons",
"supervisor.api",
"supervisor.dbus",
"supervisor.discovery",
"supervisor.discovery.services",
"supervisor.services",
"supervisor.services.modules",
"supervisor.homeassistant",
"supervisor.host",
"supervisor.misc",
"supervisor.utils",
"supervisor.plugins",
"supervisor.snapshots",
"supervisor.store",
],
include_package_data=True,
)

View File

@ -5,7 +5,7 @@ import logging
import tarfile
from typing import Dict, List, Optional, Union
from ..const import BOOT_AUTO, STATE_STARTED, AddonStartup
from ..const import BOOT_AUTO, AddonStartup, AddonState
from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import (
AddonsError,
@ -108,7 +108,7 @@ class AddonManager(CoreSysAttributes):
"""Shutdown addons."""
tasks: List[Addon] = []
for addon in self.installed:
if await addon.state() != STATE_STARTED or addon.startup != stage:
if addon.state != AddonState.STARTED or addon.startup != stage:
continue
tasks.append(addon)
@ -153,9 +153,9 @@ class AddonManager(CoreSysAttributes):
try:
await addon.instance.install(store.version, store.image)
except DockerAPIError:
except DockerAPIError as err:
self.data.uninstall(addon)
raise AddonsError()
raise AddonsError() from err
else:
self.local[slug] = addon
@ -174,8 +174,10 @@ class AddonManager(CoreSysAttributes):
try:
await addon.instance.remove()
except DockerAPIError:
raise AddonsError()
except DockerAPIError as err:
raise AddonsError() from err
else:
addon.state = AddonState.UNKNOWN
await addon.remove_data()
@ -238,15 +240,15 @@ class AddonManager(CoreSysAttributes):
raise AddonsNotSupportedError()
# Update instance
last_state = await addon.state()
last_state: AddonState = addon.state
try:
await addon.instance.update(store.version, store.image)
# Cleanup
with suppress(DockerAPIError):
await addon.instance.cleanup()
except DockerAPIError:
raise AddonsError()
except DockerAPIError as err:
raise AddonsError() from err
else:
self.data.update(store)
_LOGGER.info("Add-on '%s' successfully updated", slug)
@ -255,7 +257,7 @@ class AddonManager(CoreSysAttributes):
await addon.install_apparmor()
# restore state
if last_state == STATE_STARTED:
if last_state == AddonState.STARTED:
await addon.start()
async def rebuild(self, slug: str) -> None:
@ -279,18 +281,18 @@ class AddonManager(CoreSysAttributes):
raise AddonsNotSupportedError()
# remove docker container but not addon config
last_state = await addon.state()
last_state: AddonState = addon.state
try:
await addon.instance.remove()
await addon.instance.install(addon.version)
except DockerAPIError:
raise AddonsError()
except DockerAPIError as err:
raise AddonsError() from err
else:
self.data.update(store)
_LOGGER.info("Add-on '%s' successfully rebuilt", slug)
# restore state
if last_state == STATE_STARTED:
if last_state == AddonState.STARTED:
await addon.start()
async def restore(self, slug: str, tar_file: tarfile.TarFile) -> None:

View File

@ -1,4 +1,5 @@
"""Init file for Supervisor add-ons."""
import asyncio
from contextlib import suppress
from copy import deepcopy
from ipaddress import IPv4Address
@ -11,6 +12,7 @@ import tarfile
from tempfile import TemporaryDirectory
from typing import Any, Awaitable, Dict, List, Optional
import aiohttp
import voluptuous as vol
from voluptuous.humanize import humanize_error
@ -35,9 +37,9 @@ from ..const import (
ATTR_USER,
ATTR_UUID,
ATTR_VERSION,
ATTR_WATCHDOG,
DNS_SUFFIX,
STATE_STARTED,
STATE_STOPPED,
AddonState,
)
from ..coresys import CoreSys
from ..docker.addon import DockerAddon
@ -50,6 +52,7 @@ from ..exceptions import (
HostAppArmorError,
JsonFileError,
)
from ..utils import check_port
from ..utils.apparmor import adjust_profile
from ..utils.json import read_json_file, write_json_file
from ..utils.tar import atomic_contents_add, secure_path
@ -64,8 +67,15 @@ RE_WEBUI = re.compile(
r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$"
)
RE_WATCHDOG = re.compile(
r"^(?:(?P<s_prefix>https?|tcp)|\[PROTO:(?P<t_proto>\w+)\])"
r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$"
)
RE_OLD_AUDIO = re.compile(r"\d+,\d+")
WATCHDOG_TIMEOUT = aiohttp.ClientTimeout(total=10)
class Addon(AddonModel):
"""Hold data for add-on inside Supervisor."""
@ -74,12 +84,24 @@ class Addon(AddonModel):
"""Initialize data holder."""
super().__init__(coresys, slug)
self.instance: DockerAddon = DockerAddon(coresys, self)
self.state: AddonState = AddonState.UNKNOWN
@property
def in_progress(self) -> bool:
"""Return True if a task is in progress."""
return self.instance.in_progress
async def load(self) -> None:
"""Async initialize of object."""
with suppress(DockerAPIError):
await self.instance.attach(tag=self.version)
# Evaluate state
if await self.instance.is_running():
self.state = AddonState.STARTED
else:
self.state = AddonState.STOPPED
@property
def ip_address(self) -> IPv4Address:
"""Return IP of add-on instance."""
@ -155,6 +177,16 @@ class Addon(AddonModel):
"""Set auto update."""
self.persist[ATTR_AUTO_UPDATE] = value
@property
def watchdog(self) -> bool:
"""Return True if watchdog is enable."""
return self.persist[ATTR_WATCHDOG]
@watchdog.setter
def watchdog(self, value: bool) -> None:
"""Set watchdog enable/disable."""
self.persist[ATTR_WATCHDOG] = value
@property
def uuid(self) -> str:
"""Return an API token for this add-on."""
@ -230,8 +262,6 @@ class Addon(AddonModel):
if not url:
return None
webui = RE_WEBUI.match(url)
if not webui:
return None
# extract arguments
t_port = webui.group("t_port")
@ -245,10 +275,6 @@ class Addon(AddonModel):
else:
port = self.ports.get(f"{t_port}/tcp", t_port)
# for interface config or port lists
if isinstance(port, (tuple, list)):
port = port[-1]
# lookup the correct protocol from config
if t_proto:
proto = "https" if self.options.get(t_proto) else "http"
@ -353,13 +379,55 @@ class Addon(AddonModel):
"""Save data of add-on."""
self.sys_addons.data.save_data()
async def watchdog_application(self) -> bool:
"""Return True if application is running."""
url = super().watchdog
if not url:
return True
application = RE_WATCHDOG.match(url)
# extract arguments
t_port = application.group("t_port")
t_proto = application.group("t_proto")
s_prefix = application.group("s_prefix") or ""
s_suffix = application.group("s_suffix") or ""
# search host port for this docker port
if self.host_network:
port = self.ports.get(f"{t_port}/tcp", t_port)
else:
port = t_port
# TCP monitoring
if s_prefix == "tcp":
return await self.sys_run_in_executor(check_port, self.ip_address, port)
# lookup the correct protocol from config
if t_proto:
proto = "https" if self.options.get(t_proto) else "http"
else:
proto = s_prefix
# Make HTTP request
try:
url = f"{proto}://{self.ip_address}:{port}{s_suffix}"
async with self.sys_websession_ssl.get(
url, timeout=WATCHDOG_TIMEOUT
) as req:
if req.status < 300:
return True
except (asyncio.TimeoutError, aiohttp.ClientError):
pass
return False
async def write_options(self) -> None:
"""Return True if add-on options is written to data."""
schema = self.schema
options = self.options
# Update secrets for validation
await self.sys_secrets.reload()
await self.sys_homeassistant.secrets.reload()
try:
options = schema(options)
@ -462,12 +530,6 @@ class Addon(AddonModel):
return False
return True
async def state(self) -> str:
"""Return running state of add-on."""
if await self.instance.is_running():
return STATE_STARTED
return STATE_STOPPED
async def start(self) -> None:
"""Set options and start add-on."""
if await self.instance.is_running():
@ -488,15 +550,21 @@ class Addon(AddonModel):
# Start Add-on
try:
await self.instance.run()
except DockerAPIError:
raise AddonsError()
except DockerAPIError as err:
self.state = AddonState.ERROR
raise AddonsError() from err
else:
self.state = AddonState.STARTED
async def stop(self) -> None:
"""Stop add-on."""
try:
return await self.instance.stop()
except DockerAPIError:
raise AddonsError()
except DockerAPIError as err:
self.state = AddonState.ERROR
raise AddonsError() from err
else:
self.state = AddonState.STOPPED
async def restart(self) -> None:
"""Restart add-on."""
@ -511,12 +579,19 @@ class Addon(AddonModel):
"""
return self.instance.logs()
def is_running(self) -> Awaitable[bool]:
"""Return True if Docker container is running.
Return a coroutine.
"""
return self.instance.is_running()
async def stats(self) -> DockerStats:
"""Return stats of container."""
try:
return await self.instance.stats()
except DockerAPIError:
raise AddonsError()
except DockerAPIError as err:
raise AddonsError() from err
async def write_stdin(self, data) -> None:
"""Write data to add-on stdin.
@ -529,8 +604,8 @@ class Addon(AddonModel):
try:
return await self.instance.write_stdin(data)
except DockerAPIError:
raise AddonsError()
except DockerAPIError as err:
raise AddonsError() from err
async def snapshot(self, tar_file: tarfile.TarFile) -> None:
"""Snapshot state of an add-on."""
@ -541,31 +616,31 @@ class Addon(AddonModel):
if self.need_build:
try:
await self.instance.export_image(temp_path.joinpath("image.tar"))
except DockerAPIError:
raise AddonsError()
except DockerAPIError as err:
raise AddonsError() from err
data = {
ATTR_USER: self.persist,
ATTR_SYSTEM: self.data,
ATTR_VERSION: self.version,
ATTR_STATE: await self.state(),
ATTR_STATE: self.state,
}
# Store local configs/state
try:
write_json_file(temp_path.joinpath("addon.json"), data)
except JsonFileError:
except JsonFileError as err:
_LOGGER.error("Can't save meta for %s", self.slug)
raise AddonsError()
raise AddonsError() from err
# Store AppArmor Profile
if self.sys_host.apparmor.exists(self.slug):
profile = temp_path.joinpath("apparmor.txt")
try:
self.sys_host.apparmor.backup_profile(self.slug, profile)
except HostAppArmorError:
except HostAppArmorError as err:
_LOGGER.error("Can't backup AppArmor profile")
raise AddonsError()
raise AddonsError() from err
# write into tarfile
def _write_tarfile():
@ -588,7 +663,7 @@ class Addon(AddonModel):
await self.sys_run_in_executor(_write_tarfile)
except (tarfile.TarError, OSError) as err:
_LOGGER.error("Can't write tarfile %s: %s", tar_file, err)
raise AddonsError()
raise AddonsError() from err
_LOGGER.info("Finish snapshot for addon %s", self.slug)
@ -605,13 +680,13 @@ class Addon(AddonModel):
await self.sys_run_in_executor(_extract_tarfile)
except tarfile.TarError as err:
_LOGGER.error("Can't read tarfile %s: %s", tar_file, err)
raise AddonsError()
raise AddonsError() from err
# Read snapshot data
try:
data = read_json_file(Path(temp, "addon.json"))
except JsonFileError:
raise AddonsError()
except JsonFileError as err:
raise AddonsError() from err
# Validate
try:
@ -622,7 +697,7 @@ class Addon(AddonModel):
self.slug,
humanize_error(data, err),
)
raise AddonsError()
raise AddonsError() from err
# If available
if not self._available(data[ATTR_SYSTEM]):
@ -669,21 +744,21 @@ class Addon(AddonModel):
await self.sys_run_in_executor(_restore_data)
except shutil.Error as err:
_LOGGER.error("Can't restore origin data: %s", err)
raise AddonsError()
raise AddonsError() from err
# 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:
except HostAppArmorError as err:
_LOGGER.error(
"Can't restore AppArmor profile for add-on %s", self.slug
)
raise AddonsError()
raise AddonsError() from err
# Run add-on
if data[ATTR_STATE] == STATE_STARTED:
if data[ATTR_STATE] == AddonState.STARTED:
return await self.start()
_LOGGER.info("Finish restore for add-on %s", self.slug)

View File

@ -61,11 +61,12 @@ from ..const import (
ATTR_USB,
ATTR_VERSION,
ATTR_VIDEO,
ATTR_WATCHDOG,
ATTR_WEBUI,
SECURITY_DEFAULT,
SECURITY_DISABLE,
SECURITY_PROFILE,
AddonStages,
AddonStage,
AddonStartup,
)
from ..coresys import CoreSys, CoreSysAttributes
@ -206,7 +207,7 @@ class AddonModel(CoreSysAttributes, ABC):
return self.data[ATTR_ADVANCED]
@property
def stage(self) -> AddonStages:
def stage(self) -> AddonStage:
"""Return stage mode of add-on."""
return self.data[ATTR_STAGE]
@ -248,6 +249,11 @@ class AddonModel(CoreSysAttributes, ABC):
"""Return URL to webui or None."""
return self.data.get(ATTR_WEBUI)
@property
def watchdog(self) -> Optional[str]:
"""Return URL to for watchdog or None."""
return self.data.get(ATTR_WATCHDOG)
@property
def ingress_port(self) -> Optional[int]:
"""Return Ingress port."""

View File

@ -80,22 +80,22 @@ from ..const import (
ATTR_UUID,
ATTR_VERSION,
ATTR_VIDEO,
ATTR_WATCHDOG,
ATTR_WEBUI,
BOOT_AUTO,
BOOT_MANUAL,
PRIVILEGED_ALL,
ROLE_ALL,
ROLE_DEFAULT,
STATE_STARTED,
STATE_STOPPED,
AddonStages,
AddonStage,
AddonStartup,
AddonState,
)
from ..coresys import CoreSys
from ..discovery.validate import valid_discovery_service
from ..validate import (
DOCKER_PORTS,
DOCKER_PORTS_DESCRIPTION,
docker_ports,
docker_ports_description,
network_port,
token,
uuid_match,
@ -196,9 +196,12 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
vol.Required(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
vol.Optional(ATTR_INIT, default=True): vol.Boolean(),
vol.Optional(ATTR_ADVANCED, default=False): vol.Boolean(),
vol.Optional(ATTR_STAGE, default=AddonStages.STABLE): vol.Coerce(AddonStages),
vol.Optional(ATTR_PORTS): DOCKER_PORTS,
vol.Optional(ATTR_PORTS_DESCRIPTION): DOCKER_PORTS_DESCRIPTION,
vol.Optional(ATTR_STAGE, default=AddonStage.STABLE): vol.Coerce(AddonStage),
vol.Optional(ATTR_PORTS): docker_ports,
vol.Optional(ATTR_PORTS_DESCRIPTION): docker_ports_description,
vol.Optional(ATTR_WATCHDOG): vol.Match(
r"^(?:https?|\[PROTO:\w+\]|tcp):\/\/\[HOST\]:\[PORT:\d+\].*$"
),
vol.Optional(ATTR_WEBUI): vol.Match(
r"^(?:https?|\[PROTO:\w+\]):\/\/\[HOST\]:\[PORT:\d+\].*$"
),
@ -301,11 +304,12 @@ SCHEMA_ADDON_USER = vol.Schema(
vol.Optional(ATTR_OPTIONS, default=dict): dict,
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
vol.Optional(ATTR_NETWORK): DOCKER_PORTS,
vol.Optional(ATTR_NETWORK): docker_ports,
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_PROTECTED, default=True): vol.Boolean(),
vol.Optional(ATTR_INGRESS_PANEL, default=False): vol.Boolean(),
vol.Optional(ATTR_WATCHDOG, default=False): vol.Boolean(),
},
extra=vol.REMOVE_EXTRA,
)
@ -331,7 +335,7 @@ SCHEMA_ADDON_SNAPSHOT = vol.Schema(
{
vol.Required(ATTR_USER): SCHEMA_ADDON_USER,
vol.Required(ATTR_SYSTEM): SCHEMA_ADDON_SYSTEM,
vol.Required(ATTR_STATE): vol.In([STATE_STARTED, STATE_STOPPED]),
vol.Required(ATTR_STATE): vol.Coerce(AddonState),
vol.Required(ATTR_VERSION): vol.Coerce(str),
},
extra=vol.REMOVE_EXTRA,
@ -364,7 +368,7 @@ def validate_options(coresys: CoreSys, raw_schema: Dict[str, Any]):
# normal value
options[key] = _single_validate(coresys, typ, value, key)
except (IndexError, KeyError):
raise vol.Invalid(f"Type error for {key}")
raise vol.Invalid(f"Type error for {key}") from None
_check_missing_options(raw_schema, options, "root")
return options
@ -378,20 +382,20 @@ def _single_validate(coresys: CoreSys, typ: str, value: Any, key: str):
"""Validate a single element."""
# if required argument
if value is None:
raise vol.Invalid(f"Missing required option '{key}'")
raise vol.Invalid(f"Missing required option '{key}'") from None
# Lookup secret
if str(value).startswith("!secret "):
secret: str = value.partition(" ")[2]
value = coresys.secrets.get(secret)
if value is None:
raise vol.Invalid(f"Unknown secret {secret}")
raise vol.Invalid(f"Unknown secret {secret}") from None
# parse extend data from type
match = RE_SCHEMA_ELEMENT.match(typ)
if not match:
raise vol.Invalid(f"Unknown type {typ}")
raise vol.Invalid(f"Unknown type {typ}") from None
# prepare range
range_args = {}
@ -419,7 +423,7 @@ def _single_validate(coresys: CoreSys, typ: str, value: Any, key: str):
elif typ.startswith(V_LIST):
return vol.In(match.group("list").split("|"))(str(value))
raise vol.Invalid(f"Fatal error for {key} type {typ}")
raise vol.Invalid(f"Fatal error for {key} type {typ}") from None
def _nested_validate_list(coresys, typ, data_list, key):
@ -428,7 +432,7 @@ def _nested_validate_list(coresys, typ, data_list, key):
# Make sure it is a list
if not isinstance(data_list, list):
raise vol.Invalid(f"Invalid list for {key}")
raise vol.Invalid(f"Invalid list for {key}") from None
# Process list
for element in data_list:
@ -448,7 +452,7 @@ def _nested_validate_dict(coresys, typ, data_dict, key):
# Make sure it is a dict
if not isinstance(data_dict, dict):
raise vol.Invalid(f"Invalid dict for {key}")
raise vol.Invalid(f"Invalid dict for {key}") from None
# Process dict
for c_key, c_value in data_dict.items():
@ -475,7 +479,7 @@ def _check_missing_options(origin, exists, root):
for miss_opt in missing:
if isinstance(origin[miss_opt], str) and origin[miss_opt].endswith("?"):
continue
raise vol.Invalid(f"Missing option {miss_opt} in {root}")
raise vol.Invalid(f"Missing option {miss_opt} in {root}") from None
def schema_ui_options(raw_schema: Dict[str, Any]) -> List[Dict[str, Any]]:

View File

@ -18,6 +18,7 @@ from .host import APIHost
from .info import APIInfo
from .ingress import APIIngress
from .multicast import APIMulticast
from .network import APINetwork
from .os import APIOS
from .proxy import APIProxy
from .security import SecurityMiddleware
@ -54,6 +55,7 @@ class RestAPI(CoreSysAttributes):
self._register_os()
self._register_cli()
self._register_multicast()
self._register_network()
self._register_hardware()
self._register_homeassistant()
self._register_proxy()
@ -89,6 +91,24 @@ class RestAPI(CoreSysAttributes):
]
)
def _register_network(self) -> None:
"""Register network functions."""
api_network = APINetwork()
api_network.coresys = self.coresys
self.webapp.add_routes(
[
web.get("/network/info", api_network.info),
web.get(
"/network/interface/{interface}/info", api_network.interface_info
),
web.post(
"/network/interface/{interface}/update",
api_network.interface_update,
),
]
)
def _register_os(self) -> None:
"""Register OS functions."""
api_os = APIOS()

View File

@ -85,6 +85,7 @@ from ..const import (
ATTR_VERSION,
ATTR_VERSION_LATEST,
ATTR_VIDEO,
ATTR_WATCHDOG,
ATTR_WEBUI,
BOOT_AUTO,
BOOT_MANUAL,
@ -92,12 +93,12 @@ from ..const import (
CONTENT_TYPE_PNG,
CONTENT_TYPE_TEXT,
REQUEST_FROM,
STATE_NONE,
AddonState,
)
from ..coresys import CoreSysAttributes
from ..docker.stats import DockerStats
from ..exceptions import APIError
from ..validate import DOCKER_PORTS
from ..validate import docker_ports
from .utils import api_process, api_process_raw, api_validate
_LOGGER: logging.Logger = logging.getLogger(__name__)
@ -108,11 +109,12 @@ SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
SCHEMA_OPTIONS = vol.Schema(
{
vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
vol.Optional(ATTR_NETWORK): vol.Maybe(DOCKER_PORTS),
vol.Optional(ATTR_NETWORK): vol.Maybe(docker_ports),
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_INGRESS_PANEL): vol.Boolean(),
vol.Optional(ATTR_WATCHDOG): vol.Boolean(),
}
)
@ -213,7 +215,7 @@ class APIAddons(CoreSysAttributes):
ATTR_MACHINE: addon.supported_machine,
ATTR_HOMEASSISTANT: addon.homeassistant_version,
ATTR_URL: addon.url,
ATTR_STATE: STATE_NONE,
ATTR_STATE: AddonState.UNKNOWN,
ATTR_DETACHED: addon.is_detached,
ATTR_AVAILABLE: addon.available,
ATTR_BUILD: addon.need_build,
@ -255,12 +257,13 @@ class APIAddons(CoreSysAttributes):
ATTR_INGRESS_URL: None,
ATTR_INGRESS_PORT: None,
ATTR_INGRESS_PANEL: None,
ATTR_WATCHDOG: None,
}
if isinstance(addon, Addon) and addon.is_installed:
data.update(
{
ATTR_STATE: await addon.state(),
ATTR_STATE: addon.state,
ATTR_WEBUI: addon.webui,
ATTR_INGRESS_ENTRY: addon.ingress_entry,
ATTR_INGRESS_URL: addon.ingress_url,
@ -271,6 +274,7 @@ class APIAddons(CoreSysAttributes):
ATTR_AUTO_UPDATE: addon.auto_update,
ATTR_IP_ADDRESS: str(addon.ip_address),
ATTR_VERSION: addon.version,
ATTR_WATCHDOG: addon.watchdog,
}
)
@ -282,7 +286,7 @@ class APIAddons(CoreSysAttributes):
addon = self._extract_addon_installed(request)
# Update secrets for validation
await self.sys_secrets.reload()
await self.sys_homeassistant.secrets.reload()
# Extend schema with add-on specific validation
addon_schema = SCHEMA_OPTIONS.extend(
@ -306,6 +310,8 @@ class APIAddons(CoreSysAttributes):
if ATTR_INGRESS_PANEL in body:
addon.ingress_panel = body[ATTR_INGRESS_PANEL]
await self.sys_ingress.update_hass_panel(addon)
if ATTR_WATCHDOG in body:
addon.watchdog = body[ATTR_WATCHDOG]
addon.save_persist()

View File

@ -117,7 +117,7 @@ class APIHomeAssistant(CoreSysAttributes):
@api_process
async def stats(self, request: web.Request) -> Dict[Any, str]:
"""Return resource information."""
stats = await self.sys_homeassistant.stats()
stats = await self.sys_homeassistant.core.stats()
if not stats:
raise APIError("No stats available")
@ -138,36 +138,36 @@ class APIHomeAssistant(CoreSysAttributes):
body = await api_validate(SCHEMA_VERSION, request)
version = body.get(ATTR_VERSION, self.sys_homeassistant.latest_version)
await asyncio.shield(self.sys_homeassistant.update(version))
await asyncio.shield(self.sys_homeassistant.core.update(version))
@api_process
def stop(self, request: web.Request) -> Awaitable[None]:
"""Stop Home Assistant."""
return asyncio.shield(self.sys_homeassistant.stop())
return asyncio.shield(self.sys_homeassistant.core.stop())
@api_process
def start(self, request: web.Request) -> Awaitable[None]:
"""Start Home Assistant."""
return asyncio.shield(self.sys_homeassistant.start())
return asyncio.shield(self.sys_homeassistant.core.start())
@api_process
def restart(self, request: web.Request) -> Awaitable[None]:
"""Restart Home Assistant."""
return asyncio.shield(self.sys_homeassistant.restart())
return asyncio.shield(self.sys_homeassistant.core.restart())
@api_process
def rebuild(self, request: web.Request) -> Awaitable[None]:
"""Rebuild Home Assistant."""
return asyncio.shield(self.sys_homeassistant.rebuild())
return asyncio.shield(self.sys_homeassistant.core.rebuild())
@api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request: web.Request) -> Awaitable[bytes]:
"""Return Home Assistant Docker logs."""
return self.sys_homeassistant.logs()
return self.sys_homeassistant.core.logs()
@api_process
async def check(self, request: web.Request) -> None:
"""Check configuration of Home Assistant."""
result = await self.sys_homeassistant.check_config()
result = await self.sys_homeassistant.core.check_config()
if not result.valid:
raise APIError(result.log)

View File

@ -1,6 +1,5 @@
"""Init file for Supervisor host RESTful API."""
import asyncio
import logging
from typing import Awaitable
from aiohttp import web
@ -26,8 +25,6 @@ from ..const import (
from ..coresys import CoreSysAttributes
from .utils import api_process, api_process_raw, api_validate
_LOGGER: logging.Logger = logging.getLogger(__name__)
SERVICE = "service"
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_HOSTNAME): vol.Coerce(str)})

98
supervisor/api/network.py Normal file
View File

@ -0,0 +1,98 @@
"""REST API for network."""
import asyncio
from typing import Any, Dict
from aiohttp import web
import voluptuous as vol
from ..const import (
ATTR_ADDRESS,
ATTR_DNS,
ATTR_GATEWAY,
ATTR_ID,
ATTR_INTERFACE,
ATTR_INTERFACES,
ATTR_IP_ADDRESS,
ATTR_METHOD,
ATTR_METHODS,
ATTR_NAMESERVERS,
ATTR_PRIMARY,
ATTR_TYPE,
)
from ..coresys import CoreSysAttributes
from ..dbus.const import InterfaceMethodSimple
from ..dbus.network.interface import NetworkInterface
from ..dbus.network.utils import int2ip
from ..exceptions import APIError
from .utils import api_process, api_validate
SCHEMA_UPDATE = vol.Schema(
{
vol.Optional(ATTR_ADDRESS): vol.Coerce(str),
vol.Optional(ATTR_METHOD): vol.In(ATTR_METHODS),
vol.Optional(ATTR_GATEWAY): vol.Coerce(str),
vol.Optional(ATTR_DNS): [str],
}
)
def interface_information(interface: NetworkInterface) -> dict:
"""Return a dict with information of a interface to be used in th API."""
return {
ATTR_IP_ADDRESS: f"{interface.ip_address}/{interface.prefix}",
ATTR_GATEWAY: interface.gateway,
ATTR_ID: interface.id,
ATTR_TYPE: interface.type,
ATTR_NAMESERVERS: [int2ip(x) for x in interface.nameservers],
ATTR_METHOD: InterfaceMethodSimple.DHCP
if interface.method == "auto"
else InterfaceMethodSimple.STATIC,
ATTR_PRIMARY: interface.primary,
}
class APINetwork(CoreSysAttributes):
"""Handle REST API for network."""
@api_process
async def info(self, request: web.Request) -> Dict[str, Any]:
"""Return network information."""
interfaces = {}
for interface in self.sys_host.network.interfaces:
interfaces[
self.sys_host.network.interfaces[interface].name
] = interface_information(self.sys_host.network.interfaces[interface])
return {ATTR_INTERFACES: interfaces}
@api_process
async def interface_info(self, request: web.Request) -> Dict[str, Any]:
"""Return network information for a interface."""
req_interface = request.match_info.get(ATTR_INTERFACE)
for interface in self.sys_host.network.interfaces:
if req_interface == self.sys_host.network.interfaces[interface].name:
return interface_information(
self.sys_host.network.interfaces[interface]
)
return {}
@api_process
async def interface_update(self, request: web.Request) -> Dict[str, Any]:
"""Update the configuration of an interface."""
req_interface = request.match_info.get(ATTR_INTERFACE)
if not self.sys_host.network.interfaces.get(req_interface):
raise APIError(f"Interface {req_interface} does not exsist")
args = await api_validate(SCHEMA_UPDATE, request)
if not args:
raise APIError("You need to supply at least one option to update")
await asyncio.shield(
self.sys_host.network.interfaces[req_interface].update_settings(**args)
)
await asyncio.shield(self.sys_host.network.update())
return await asyncio.shield(self.interface_info(request))

View File

@ -1,9 +1,9 @@
try {
new Function("import('/api/hassio/app/frontend_latest/entrypoint.855567b9.js')")();
new Function("import('/api/hassio/app/frontend_latest/entrypoint.cfec6eb5.js')")();
} catch (err) {
var el = document.createElement('script');
el.src = '/api/hassio/app/frontend_es5/entrypoint.19035830.js';
el.src = '/api/hassio/app/frontend_es5/entrypoint.9b944a05.js';
document.body.appendChild(el);
}

View File

@ -0,0 +1,2 @@
(self.webpackJsonp=self.webpackJsonp||[]).push([[1],{177:function(e,r,n){"use strict";n.r(r),n.d(r,"codeMirror",(function(){return c})),n.d(r,"codeMirrorCss",(function(){return i}));var a=n(165),o=n.n(a),s=n(173),t=(n(174),n(175),n(10));o.a.commands.save=function(e){Object(t.a)(e.getWrapperElement(),"editor-save")};var c=o.a,i=s.a}}]);
//# sourceMappingURL=chunk.0d1dbeb0d1afbd18f64e.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.0d1dbeb0d1afbd18f64e.js","sources":["webpack:///chunk.0d1dbeb0d1afbd18f64e.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.1edd93e4d4c4749e55ab.js","sources":["webpack:///chunk.1edd93e4d4c4749e55ab.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,3 @@
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <feross@feross.org> <http://feross.org>
* @license MIT
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.1f998a3d7e32b7b3c45c.js","sources":["webpack:///chunk.1f998a3d7e32b7b3c45c.js"],"mappings":";AAAA","sourceRoot":""}

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.2dfe3739f6cdf06691c3.js","sources":["webpack:///chunk.2dfe3739f6cdf06691c3.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.2e9e45ecb0d870d27d88.js","sources":["webpack:///chunk.2e9e45ecb0d870d27d88.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.4a9b56271bdf6fea0f78.js","sources":["webpack:///chunk.4a9b56271bdf6fea0f78.js"],"mappings":"AAAA","sourceRoot":""}

View File

@ -1,2 +0,0 @@
(self.webpackJsonp=self.webpackJsonp||[]).push([[1],{168:function(e,r,n){"use strict";n.r(r),n.d(r,"codeMirror",(function(){return c})),n.d(r,"codeMirrorCss",(function(){return i}));var a=n(127),o=n.n(a),s=n(164),t=(n(165),n(166),n(8));o.a.commands.save=function(e){Object(t.a)(e.getWrapperElement(),"editor-save")};var c=o.a,i=s.a}}]);
//# sourceMappingURL=chunk.53ba85527e846b37953a.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.53ba85527e846b37953a.js","sources":["webpack:///chunk.53ba85527e846b37953a.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.5bff531a8ac4ea5d0e8f.js","sources":["webpack:///chunk.5bff531a8ac4ea5d0e8f.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.67ebcbc4ba9d39e9c2b9.js","sources":["webpack:///chunk.67ebcbc4ba9d39e9c2b9.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.6abfd4fdbd7486588838.js","sources":["webpack:///chunk.6abfd4fdbd7486588838.js"],"mappings":"AAAA","sourceRoot":""}

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.7ef6bcb43647bc158e88.js","sources":["webpack:///chunk.7ef6bcb43647bc158e88.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.937f66fb08e0d4aa8818.js","sources":["webpack:///chunk.937f66fb08e0d4aa8818.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.9de6943cce77c49b4586.js","sources":["webpack:///chunk.9de6943cce77c49b4586.js"],"mappings":";AAAA","sourceRoot":""}

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.adad578706ef56ae55ba.js","sources":["webpack:///chunk.adad578706ef56ae55ba.js"],"mappings":"AAAA","sourceRoot":""}

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.d3a71177023db5bc7440.js","sources":["webpack:///chunk.d3a71177023db5bc7440.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.dacd9533a16ebaba94e9.js","sources":["webpack:///chunk.dacd9533a16ebaba94e9.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.e4f91d8c5f0772ea87e5.js","sources":["webpack:///chunk.e4f91d8c5f0772ea87e5.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.f60d6d63bed838a42b65.js","sources":["webpack:///chunk.f60d6d63bed838a42b65.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"entrypoint.19035830.js","sources":["webpack:///entrypoint.19035830.js"],"mappings":";AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,10 @@
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <http://feross.org>
* @license MIT
*/
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
@ -13,6 +20,24 @@ See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/**
* @license
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
@ -104,6 +129,23 @@ and limitations under the License.
* THE SOFTWARE.
*/
/**
* @license
* Copyright 2018 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @license
* Copyright 2019 Google Inc.

View File

@ -0,0 +1 @@
{"version":3,"file":"entrypoint.9b944a05.js","sources":["webpack:///entrypoint.9b944a05.js"],"mappings":";AAAA","sourceRoot":""}

View File

@ -1,3 +1,3 @@
{
"entrypoint.js": "/api/hassio/app/frontend_es5/entrypoint.19035830.js"
"entrypoint.js": "/api/hassio/app/frontend_es5/entrypoint.9b944a05.js"
}

View File

@ -1 +1 @@
{"version":3,"file":"chunk.d7009039727fd352acb9.js","sources":["webpack:///chunk.d7009039727fd352acb9.js"],"mappings":"AAAA;;;AA4LA;AACA;;;AAGA;;;;;AAKA;;AAEA;AAEA;AACA;;;;;;;AAQA;;;;;AAKA;;AAEA;AAEA;AACA;;;;;;;AAQA;;;AAKA;;;;;;;;;;;;;;;;AAuBA;AAohBA;;AAEA;;AAEA;AACA;;AAIA;AAwIA;;;;AAIA;;AAEA;AACA;;;AAGA;;;;AAIA;AACA;;;;;;AAQA;;;;;;;;;;;;;;;;;;;;;;AA6BA;;;AAkMA;AACA;;;;;;;;AAQA;;AAGA;;;AAGA;;AAEA;AACA;;;;AAIA;;;;;;;AAQA;;;AAGA;;;AAvCA;;;;;;;;;;;;;;;AAkEA;;;AAwLA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;;AApBA;;;;;;;;;;;AA4CA;;;AA6GA;;AAEA;;;;AARA;;;;;;;;;;;;AAiCA;;;;AA8HA;;;AAMA;AACA;;;AAGA;;AAEA;;AAKA;;AAEA;;AAEA;;AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6EA;AA6LA;;;;AAIA;AACA;AACA;AACA;;;AAGA;;;;;;;;AAQA;AACA;AACA;;;;AAIA;AACA;;;AAGA;;;AAGA;AACA;;;;;;;AAOA;;;;;;;;;AASA;;AAEA;AACA;;;;AAIA;;AAEA;;;;AAIA;;;AAGA;;;;AAIA;AACA;AACA;;;AAGA;;;;;;AAMA;;AAEA;AACA;;;;AAIA;;;AAGA;;AAEA;;AAEA;AACA;AAIA;;;;;;AAMA;;AAEA;AACA;;AAEA;AAKA;;AAEA;;;;AAIA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;;;AAGA;;AAEA;;AAEA;AACA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;AACA;;;;;AAKA;;AAEA;AACA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;;;;;;AAMA;;;AAGA;;;AAGA;;;;AAIA;AACA;;;;AAIA;;;;AAIA;AACA;;;;AAIA;AACA;;;;AAIA;AACA;AACA;;;AAGA;;;;;AAKA;;AAEA;AACA;;;;;AAKA;;;;;;;AAOA;AACA;;;;AAIA;AACA;AACA;;;AAGA;AACA;;;AAGA;AACA;;;;;;AAMA;AACA;;;;AAIA;;AAEA;AACA;;;;;AAKA;;AAEA;;;;;;;;;;AAUA;AACA;AACA;;;AAGA;;;AAGA;;;;AAIA;;;AAGA;AACA;;;;AAIA;AACA;AACA;;;;;;AAMA;AACA;AACA;;;;;;;;AAQA;;;;AAIA;;;;AAIA;AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkIA;;;AA0UA;AACA;AACA;;;AARA;;;;;;AA4BA;AAoGA;;AAEA;;AAEA;AACA;AACA;;;AAGA;;;AAKA;;;;;;;;;AAgBA;;;AAmGA;AACA;;;AAPA;;;;;;AA2BA;;AAmQA;AACA;AACA;AACA;;AAEA;;AAEA;;AAEA;AACA;AACA;AACA;;;AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA","sourceRoot":""}
{"version":3,"file":"chunk.169d772822215fb022c3.js","sources":["webpack:///chunk.169d772822215fb022c3.js"],"mappings":"AAAA;;;AA4LA;AACA;;;AAGA;;;;;AAKA;;AAEA;AAEA;AACA;;;;;;;AAQA;;;;;AAKA;;AAEA;AAEA;AACA;;;;;;;AAQA;;;AAKA;;;;;;;;;;;;;;;;AAuBA;AA6mBA;;AAEA;;AAEA;AACA;;AAIA;AAwIA;;;;AAIA;;AAEA;AACA;;;AAGA;;;;AAIA;AACA;;;;;;AAQA;;;;;;;;;;;;;;;;;;;;;;AA6BA;;;AAkMA;AACA;;;;;;;;AAQA;;AAGA;;;AAGA;;AAEA;AACA;;;;AAIA;;;;;;;AAQA;;;AAGA;;;AAvCA;;;;;;;;;;;;;;;AAkEA;;;AAwLA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;;AApBA;;;;;;;;;;;AA4CA;;;AA6GA;;AAEA;;;;AARA;;;;;;;;;;;;AAiCA;;;;AA8HA;;;AAMA;AACA;;;AAGA;;AAEA;;AAKA;;AAEA;;AAEA;;AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6EA;AAiMA;;;;AAIA;AACA;AACA;AACA;;;AAGA;;;;;;;;AAQA;AACA;AACA;;;;AAIA;AACA;;;AAGA;;;AAGA;AACA;;;;;;;AAOA;;;;;;;;;AASA;;AAEA;AACA;;;;AAIA;;AAEA;;;;AAIA;;;AAGA;;;;AAIA;AACA;AACA;;;AAGA;;;;;;AAMA;;AAEA;AACA;;;;AAIA;;;AAGA;;AAEA;;AAEA;AACA;AAIA;;;;;;AAMA;;AAEA;AACA;;AAEA;AAKA;;AAEA;;;;AAIA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;;;AAGA;;AAEA;;AAEA;AACA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;AACA;;;;;AAKA;;AAEA;AACA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;;;;;;AAMA;;;AAGA;;;AAGA;;AAEA;;;;;;;;AAQA;AACA;;;;;AAKA;AACA;;;;;;;;AAQA;AACA;;;;AAIA;AACA;AACA;;;;;;;;;AASA;AACA;;;;AAIA;AACA;AACA;;;;;AAKA;;;AAGA;AACA;AACA;;;;AAIA;AACA;AACA;;;;;;;;AAQA;AACA;;;;AAIA;;AAEA;AACA;;;AAGA;AACA;;;AAGA;AACA;;;;;;AAMA;AACA;;;;AAIA;;AAEA;AACA;;;;;AAKA;;AAEA;;;;;;;;;;AAUA;AACA;AACA;;;AAGA;;;AAGA;;;;AAIA;;;AAGA;AACA;;;;AAIA;AACA;AACA;;;;;;AAMA;AACA;AACA;;;;;;;;AAQA;;;;AAIA;;;;AAIA;AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwZA;;;AAoFA;AACA;AACA;;;AARA;;;;;;AA4BA;AAoGA;;AAEA;;AAEA;AACA;AACA;;;AAGA;;;AAKA;;;;;;;;;AAgBA;;;AAmGA;AACA;;;AAPA;;;;;;AA2BA;;AAmQA;AACA;AACA;AACA;;AAEA;;AAEA;;AAEA;AACA;AACA;AACA;;;AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.19fdeb1dc19e9028c877.js","sources":["webpack:///chunk.19fdeb1dc19e9028c877.js"],"mappings":"AAAA;AA0OA;AACA;AACA;AANA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEA","sourceRoot":""}

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.1dcdd03de4add9e1f326.js","sources":["webpack:///chunk.1dcdd03de4add9e1f326.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,3 @@
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <feross@feross.org> <http://feross.org>
* @license MIT
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.3071be0252919da82a50.js","sources":["webpack:///chunk.3071be0252919da82a50.js"],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAy7EA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkfA","sourceRoot":""}

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.3455ded08421a656fb89.js","sources":["webpack:///chunk.3455ded08421a656fb89.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.46e2dce2dcad3c8f5df8.js","sources":["webpack:///chunk.46e2dce2dcad3c8f5df8.js"],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoyFA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgrMA","sourceRoot":""}

View File

@ -1,2 +0,0 @@
(self.webpackJsonp=self.webpackJsonp||[]).push([[1],{163:function(e,r,n){"use strict";n.r(r),n.d(r,"codeMirror",(function(){return c})),n.d(r,"codeMirrorCss",(function(){return i}));var s=n(124),o=n.n(s),t=n(159),a=(n(160),n(161),n(8));o.a.commands.save=e=>{Object(a.a)(e.getWrapperElement(),"editor-save")};const c=o.a,i=t.a}}]);
//# sourceMappingURL=chunk.5066cbaab136d2561122.js.map

Some files were not shown because too many files have changed in this diff Show More