Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NAS-127428 / 24.10 / Installer HTTP API #77

Merged
merged 2 commits into from
Apr 30, 2024
Merged
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
243 changes: 243 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
# API Specification

This API uses JSON-RPC 2.0: https://www.jsonrpc.org/specification

# Server methods

## adopt

“Adopt” the system. It will return an access key that must be used to authenticate on this system when
re-connecting.

### Result jsonschema

{
"type": "string"
}

## authenticate

Authenticate the connection on the “adopted” system.

### Parameter jsonschema

{
"type": "string"
}

## install

Performs system installation.

### Parameter jsonschema

{
"type": "object",
"required": [
"disks",
"create_swap",
"set_pmbr",
"authentication"
],
"additionalProperties": false,
"properties": {
"disks": {
"type": "array",
"items": {
"type": "string"
}
},
"create_swap": {
"type": "boolean"
},
"set_pmbr": {
"type": "boolean"
},
"authentication": {
"type": [
"object",
"null"
],
"required": [
"username",
"password"
],
"additionalProperties": false,
"properties": {
"username": {
"type": "string",
"enum": [
"admin",
"root"
]
},
"password": {
"type": "string",
"minLength": 6
}
}
},
"post_install": {
"type": "object",
"additionalProperties": false,
"properties": {
"network_interfaces": {
"type": "array",
"items": {
"type": "object",
"required": [
"name"
],
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"aliases": {
"type": "array",
"items": {
"type": "object",
"required": [
"type",
"address",
"netmask"
],
"additionalProperties": false,
"properties": {
"type": {
"type": "string"
},
"address": {
"type": "string"
},
"netmask": {
"type": "integer"
}
}
}
},
"ipv4_dhcp": {
"type": "boolean"
},
"ipv6_auto": {
"type": "boolean"
}
}
}
}
}
}
}
}

## is_adopted

Returns `true` if the system in question is “adopted”, false otherwise.

The system is “adopted” when the adopt method is called. Subsequent connections to the system must call the
`authenticate` method before trying to do anything else.

### Result jsonschema

{
"type": "boolean"
}

## list_disks

Provides list of available disks.

### Result jsonschema

{
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"size": {
"type": "number"
},
"model": {
"type": "string"
},
"label": {
"type": "string"
},
"removable": {
"type": "boolean"
}
}
}
}

## list_network_interfaces

Provides list of available network interfaces.

### Result jsonschema

{
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}
}

## reboot

Performs system reboot.

## shutdown

Performs system shutdown.

## system_info

Provides auxiliary system information.

### Result jsonschema

{
"type": "object",
"properties": {
"installation_running": {
"type": "boolean"
},
"version": {
"type": "string"
},
"efi": {
"type": "boolean"
}
}
}

# Client methods

## installation_progress

Server calls this method on the client to report installation progress. This method will only be called
after the client initiates system installation and before the server reports its result.

### Parameter jsonschema

{
"type": "object",
"properties": {
"progress": {
"type": "number"
},
"message": {
"type": "text"
}
}
}

6 changes: 6 additions & 0 deletions debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ Maintainer: Vladimir Vinogradenko <[email protected]>
Build-Depends: debhelper-compat (= 12),
dh-python,
python3-all,
python3-aiohttp-rpc,
python3-ixhardware,
python3-jsonschema,
python3-licenselib,
python3-humanfriendly,
python3-psutil,
python3-pyroute2,
python3-setuptools
Standards-Version: 4.4.0

Expand All @@ -20,10 +23,13 @@ Depends: ${misc:Depends},
gdisk,
openzfs,
parted,
python3-aiohttp-rpc,
python3-ixhardware,
python3-jsonschema,
python3-licenselib,
python3-humanfriendly,
python3-psutil,
python3-pyroute2,
setserial,
squashfs-tools,
util-linux
Expand Down
2 changes: 2 additions & 0 deletions debian/rules
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export PYBUILD_NAME=truenas_installer
%:
dh $@ --with python3 --buildsystem=pybuild

override_dh_installsystemd:
dh_installsystemd --no-start -r --no-restart-after-upgrade --name=truenas-installer
8 changes: 8 additions & 0 deletions debian/truenas-installer.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[Unit]
Description=TrueNAS Installer

[Service]
ExecStart=/usr/bin/python3 -m truenas_installer --server

[Install]
WantedBy=multi-user.target
35 changes: 33 additions & 2 deletions truenas_installer/__main__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,44 @@
import argparse
import asyncio

from aiohttp import web

from ixhardware import parse_dmi

from .installer import Installer
from .installer_menu import InstallerMenu
from .server import InstallerRPCServer
from .server.doc import generate_api_doc


if __name__ == "__main__":
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--doc", action="store_true")
parser.add_argument("--server", action="store_true")
args = parser.parse_args()

with open("/etc/version") as f:
version = f.read().strip()

dmi = parse_dmi()

installer = Installer(version, dmi)
installer.run()

if args.doc:
generate_api_doc()
elif args.server:
rpc_server = InstallerRPCServer(installer)
app = web.Application()
app.router.add_routes([
web.get("/", rpc_server.handle_http_request),
])
app.on_shutdown.append(rpc_server.on_shutdown)
web.run_app(app, port=80)
else:
loop = asyncio.get_event_loop()
loop.create_task(InstallerMenu(installer).run())
loop.run_forever()


if __name__ == "__main__":
main()
7 changes: 4 additions & 3 deletions truenas_installer/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
BOOT_POOL = "boot-pool"


async def install(disks, create_swap, set_pmbr, authentication, sql, callback):
async def install(disks, create_swap, set_pmbr, authentication, post_install, sql, callback):
with installation_lock:
try:
if not os.path.exists("/etc/hostid"):
Expand All @@ -27,7 +27,7 @@ async def install(disks, create_swap, set_pmbr, authentication, sql, callback):
callback(0, "Creating boot pool")
await create_boot_pool([get_partition(disk, 3) for disk in disks])
try:
await run_installer(disks, authentication, sql, callback)
await run_installer(disks, authentication, post_install, sql, callback)
finally:
await run(["zpool", "export", "-f", BOOT_POOL])
except subprocess.CalledProcessError as e:
Expand Down Expand Up @@ -108,7 +108,7 @@ async def create_boot_pool(devices):
await run(["zfs", "create", "-o", "canmount=off", "-o", "mountpoint=legacy", f"{BOOT_POOL}/grub"])


async def run_installer(disks, authentication, sql, callback):
async def run_installer(disks, authentication, post_install, sql, callback):
with tempfile.TemporaryDirectory() as src:
await run(["mount", "/cdrom/TrueNAS-SCALE.update", src, "-t", "squashfs", "-o", "loop"])
try:
Expand All @@ -117,6 +117,7 @@ async def run_installer(disks, authentication, sql, callback):
"disks": disks,
"json": True,
"pool_name": BOOT_POOL,
"post_install": post_install,
"sql": sql,
"src": src,
}
Expand Down
12 changes: 2 additions & 10 deletions truenas_installer/installer.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import asyncio

from .installer_menu import InstallerMenu
import os


class Installer:
def __init__(self, version, dmi):
self.version = version
self.dmi = dmi

def run(self):
loop = asyncio.get_event_loop()

loop.create_task(InstallerMenu(self).run())

loop.run_forever()
self.efi = os.path.exists("/sys/firmware/efi")
Loading
Loading