Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 53 additions & 16 deletions bootstrap/bootstrap/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@

import docker
import requests
import urllib3
from loguru import logger

urllib3.disable_warnings()


class Bootstrapper:

Expand All @@ -24,7 +27,9 @@ class Bootstrapper:
SETTINGS_NAME_CORE = "core"
core_last_response_time = time.monotonic()

def __init__(self, client: docker.DockerClient, low_level_api: docker.APIClient = None) -> None:
def __init__(
self, client: docker.DockerClient, low_level_api: docker.APIClient = None
) -> None:
self.version_chooser_is_online = False
self.client: docker.DockerClient = client
self.core_last_response_time = time.monotonic()
Expand All @@ -37,20 +42,25 @@ def __init__(self, client: docker.DockerClient, low_level_api: docker.APIClient
def overwrite_config_file_with_defaults() -> None:
"""Overwrites the config file with the default configuration"""
try:
os.makedirs(pathlib.Path(Bootstrapper.DOCKER_CONFIG_FILE_PATH).parent, exist_ok=True)
os.makedirs(
pathlib.Path(Bootstrapper.DOCKER_CONFIG_FILE_PATH).parent, exist_ok=True
)
except Exception as exception:
raise RuntimeError(
f"Failed to create folder for configuration file: {Bootstrapper.DOCKER_CONFIG_FILE_PATH}"
) from exception

try:
shutil.copy(
Bootstrapper.DOCKER_CONFIG_FILE_PATH, Bootstrapper.DOCKER_CONFIG_FILE_PATH.with_suffix(".json.bak")
Bootstrapper.DOCKER_CONFIG_FILE_PATH,
Bootstrapper.DOCKER_CONFIG_FILE_PATH.with_suffix(".json.bak"),
)
except FileNotFoundError:
# we don't mind if the file is already there
pass
shutil.copy(Bootstrapper.DEFAULT_FILE_PATH, Bootstrapper.DOCKER_CONFIG_FILE_PATH)
shutil.copy(
Bootstrapper.DEFAULT_FILE_PATH, Bootstrapper.DOCKER_CONFIG_FILE_PATH
)

@staticmethod
def read_config_file() -> Dict[str, Any]:
Expand All @@ -63,21 +73,31 @@ def read_config_file() -> Dict[str, Any]:
# Tries to open the current file
config = {}
try:
with open(Bootstrapper.DOCKER_CONFIG_FILE_PATH, encoding="utf-8") as config_file:
with open(
Bootstrapper.DOCKER_CONFIG_FILE_PATH, encoding="utf-8"
) as config_file:
config = json.load(config_file)
assert Bootstrapper.SETTINGS_NAME_CORE in config, "missing core entry in startup.json"
assert (
Bootstrapper.SETTINGS_NAME_CORE in config
), "missing core entry in startup.json"
necessary_keys = ["image", "tag", "binds", "privileged", "network"]
for key in necessary_keys:
assert key in config[Bootstrapper.SETTINGS_NAME_CORE], f"missing key in json file: {key}"
assert (
key in config[Bootstrapper.SETTINGS_NAME_CORE]
), f"missing key in json file: {key}"

except Exception as error:
logger.error(f"unable to read startup.json file ({error}), reverting to defaults...")
logger.error(
f"unable to read startup.json file ({error}), reverting to defaults..."
)
# Copy defaults over and read again
Bootstrapper.overwrite_config_file_with_defaults()
with open(Bootstrapper.DEFAULT_FILE_PATH, encoding="utf-8") as config_file:
config = json.load(config_file)

config[Bootstrapper.SETTINGS_NAME_CORE]["binds"][str(Bootstrapper.HOST_CONFIG_PATH)] = {
config[Bootstrapper.SETTINGS_NAME_CORE]["binds"][
str(Bootstrapper.HOST_CONFIG_PATH)
] = {
"bind": str(Bootstrapper.DOCKER_CONFIG_PATH),
"mode": "rw",
}
Expand Down Expand Up @@ -121,15 +141,19 @@ def pull(self, component_name: str) -> None:
try:
self.client.images.pull(f"{image_name}:{tag}")
except Exception as exception:
logger.warning(f"Failed to pull image ({image_name}:{tag}): {exception}")
logger.warning(
f"Failed to pull image ({image_name}:{tag}): {exception}"
)
return

# if there is ncurses support, proceed with it
lines: int = 0
# map each id to a line
id_line: Dict[str, int] = {}
try:
for line in self.low_level_api.pull(f"{image_name}:{tag}", stream=True, decode=True):
for line in self.low_level_api.pull(
f"{image_name}:{tag}", stream=True, decode=True
):
if len(line.keys()) == 1 and "status" in line:
# in some cases there is only "status", print that on the last line
screen.addstr(lines, 0, line["status"])
Expand All @@ -144,7 +168,9 @@ def pull(self, component_name: str) -> None:
current_line = id_line[layer_id]
if "progress" in line:
progress = line["progress"]
screen.addstr(current_line, 0, f"[{layer_id}]\t({status})\t{progress}")
screen.addstr(
current_line, 0, f"[{layer_id}]\t({status})\t{progress}"
)
else:
screen.addstr(current_line, 0, f"[{layer_id}]\t({status})")

Expand Down Expand Up @@ -191,7 +217,9 @@ def start(self, component_name: str) -> bool:
try:
self.pull(component_name)
except docker.errors.NotFound:
warn(f"Image {image_name}:{image_version} not found, reverting to default...")
warn(
f"Image {image_name}:{image_version} not found, reverting to default..."
)
self.overwrite_config_file_with_defaults()
return False
except docker.errors.APIError as error:
Expand Down Expand Up @@ -238,7 +266,10 @@ def is_running(self, component: str) -> bool:
bool: True if the chosen container is running
"""
try:
return any(container.name.endswith(component) for container in self.client.containers.list())
return any(
container.name.endswith(component)
for container in self.client.containers.list()
)
except Exception as exception:
logger.warning(f"Could not list containers: {exception}")
return False
Expand All @@ -250,7 +281,11 @@ def is_version_chooser_online(self) -> bool:
bool: True if version chooser is online, False otherwise.
"""
try:
response = requests.get("http://localhost/version-chooser/v1.0/version/current", timeout=10)
response = requests.get(
"http://localhost/version-chooser/v1.0/version/current",
timeout=10,
verify=False,
)
if Bootstrapper.SETTINGS_NAME_CORE in response.json()["repository"]:
if not self.version_chooser_is_online:
self.version_chooser_is_online = True
Expand Down Expand Up @@ -300,7 +335,9 @@ def run(self) -> None:

# Version choose failed, time to restarted core
self.core_last_response_time = time.monotonic()
logger.warning("Core has not responded in 5 minutes, resetting to factory...")
logger.warning(
"Core has not responded in 5 minutes, resetting to factory..."
)
self.overwrite_config_file_with_defaults()
try:
if self.start(image):
Expand Down
17 changes: 17 additions & 0 deletions core/frontend/src/components/wizard/Wizard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
<div class="d-flex flex-column align-center">
<v-text-field v-model="vehicle_name" label="Vehicle Name" />
<v-text-field v-model="mdns_name" label="MDNS Name" />
<v-checkbox v-model="enable_tls" label="Enable TLS" />
</div>
<ScriptLoader
v-model="scripts"
Expand Down Expand Up @@ -344,6 +345,8 @@ export default Vue.extend({
step_number: 0,
sub_model: get_model('sub', 'bluerov'),
vehicle_name: 'blueos',
// Allow the user to enable TLS on their vehicle
enable_tls: false,
vehicle_type: '' as Vehicle | string,
vehicle_image: null as string | null,
// Allow us to check if the user is stuck in retry
Expand Down Expand Up @@ -458,6 +461,15 @@ export default Vue.extend({
skip: false,
started: false,
},
{
title: 'Set TLS',
summary: `Enable TLS for the BlueOS web server: ${this.enable_tls}`,
promise: () => this.setTLS(),
message: undefined,
done: false,
skip: false,
started: false,
},
{
title: 'Set vehicle image',
summary: 'Set image to be used for vehicle thumbnail',
Expand Down Expand Up @@ -599,6 +611,11 @@ export default Vue.extend({
.then(() => undefined)
.catch(() => 'Failed to set custom vehicle name')
},
async setTLS(): Promise<ConfigurationStatus> {
return beacon.setTLS(this.enable_tls)
.then(() => undefined)
.catch(() => 'Failed to change TLS configuration')
},
async disableWifiHotspot(): Promise<ConfigurationStatus> {
return back_axios({
method: 'post',
Expand Down
33 changes: 33 additions & 0 deletions core/frontend/src/store/beacon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ class BeaconStore extends VuexModule {

vehicle_name = ''

use_tls = false

fetchAvailableDomainsTask = new OneMoreTime(
{ delay: 5000 },
)
Expand All @@ -61,6 +63,12 @@ class BeaconStore extends VuexModule {
this.vehicle_name = vehicle_name
}

// eslint-disable-next-line
@Mutation
private _setUseTLS(use_tls: boolean): void {
this.use_tls = use_tls
}

@Mutation
setAvailableDomains(domains: Domain[]): void {
this.available_domains = domains
Expand Down Expand Up @@ -248,6 +256,31 @@ class BeaconStore extends VuexModule {
}
}, 1000)
}

@Action
async setTLS(enable_tls: boolean): Promise<boolean> {
return back_axios({
method: 'post',
url: `${this.API_URL}/use_tls`,
timeout: 5000,
params: {
enable_tls: enable_tls,
},
})
.then(() => {
// eslint-disable-next-line
this._setUseTLS(enable_tls)
return true
})
.catch((error) => {
if (isBackendOffline(error)) {
return false
}
const message = `Could not set TLS option: ${error.response?.data ?? error.message}.`
notifier.pushError('BEACON_SET_TLS_FAIL', message, true)
return false
})
}
}

export { BeaconStore }
Expand Down
Loading