Skip to content

Commit

Permalink
updating readme and bugfixes
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewAirbotics committed Aug 11, 2023
1 parent 4ea49ec commit 7c95197
Show file tree
Hide file tree
Showing 6 changed files with 32 additions and 136 deletions.
97 changes: 5 additions & 92 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
<img width="300" src="./docs/imgs/logo-light-theme.png#gh-light-mode-only"/>
</p>

<h3 align="center">Airbotics is an open-source software deployment platform for robotics.</h3>

<br />

Expand Down Expand Up @@ -40,11 +39,9 @@
</a>
<br />

![Airbotics screenshot](docs/imgs/screenshot.png)

<br />

<p align="center">⚠️ We're in Closed Beta so this may still contain bugs. ⚠️</p>
<p align="center">⚠️ This project has been archived and should not be considered stable. ⚠️</p>

<br />

Expand All @@ -58,10 +55,7 @@

## Contents

- [Features](#features)
- [Docs](#docs)
- [Project status](#project-status)
- [Community](#community)
- [Overview](#overview)
- [Contributing](#contributing)
- [Security](#security)
- [License](#license)
Expand All @@ -70,87 +64,11 @@



## Overview

This is an open-source implementation of a software update-over-the-air backend platform built on Uptane, OSTree and Yocto.


## Features


#### 1. Multiple computers per robot

A robot typically doesn't have just one computer that needs to be updated - they often have a main board, maybe a separate GPU board, and a several satellite microcontrollers - each of which may need to be updated together, to maintain API compatibility, or separately if one changes more frequently than others.

We're building our system so every computer on a robot can natively accept, verify and install updates and report on their status.


#### 2. Pull-based

Computers in robots can have poor network connections, limited bandwidth, unreliable power, or spend much of their time controlling the robot - something that shouldn't be interrupted.

It's inenvitable that some robots in a fleet won't be in a state to accept an update when one is pushed out, requring retry logic to avoid failed deployments and time spent debugging.

With Airbotics, you can create application-specific business logic on your robots that decide when an update should be initiated (e.g. in a charging dock, have good network, have shut down other processes, have notified fleet manager, etc.), and then make a [ROS](https://www.ros.org/) action call to our agent to begin an update cycle, which we report back on.

#### 3. Security

Software supply chains are increasingly being targeted by attackers. We're building on top of the [Uptane](https://uptane.github.io/) framework - the *de facto* standard for OTA in automative sector designed to be compromise-resilient. Uptane is an extension of [The Update Framework](https://theupdateframework.com/) - a framework for securing software update systems used by Docker Content Trust, PyPI, Datadog and more.

With Airbotics you can assign signing authority to multiple members of your team (e.g Head of QA, Head of Ops, Director of Engineering, etc.), a quorom of which are required to sign a release using their private offline keys in order for it to be considered ready to be deployed to your fleet.

#### 4. Multi-player

It takes a village to run a robot fleet, and lots of people need to be involved in the software release process:
- An account manager may want to request a group of robots be updated to a new version, requiring approval from other teammates,
- A QA engineer may need to sign off on a release as fit for deployment,
- Security teams may need to know which robots are affected by an outdated vulnerable release and put out a patch,
- Support teams may need to know which release is deployed to a robot that customers are reporting problems with,
- Sales and marketing teams may need to know what is the breakdown of robots on `v1.1` over `v1.0`, given that the subscription for `v1.1` has increased in price,
- And on and on...

We're building a dashboard that anyone on the team can use - without requiring coding skills - to get the information and take the actions they need without having to ping the dev team.

#### 5. Press pause

Robots can have seasonal usage - argicultural robots tend be busy around harvest time, lawnmowing robots tend to be busy during summer months, and intralogistics robots tend be busy around Black Friday. After that, robots may be unused for a period of time.

Airbotics allows you to decommission a robot for a period of time without having to re-provision it with our agent. When it's decommissioned, a robot will still be authenticated and connected to our backend, but won't be able to accept updates - robots that are decommissioned won't be charged.

#### 6. Plus more

Along with these, we're building the usual features you would expect from a OTA system - gradual / canary rollouts, integrations with your favourite tools, grouping of robots, team management, audit trails, and so on.








## Docs

A web version of the documentation is available [here](https://docs.airbotics.io), the source of which is available [here](docs/README.md).









## Project status

- [x] On the launchpad (Alpha) - moving fast and breaking things, should not be considered stable.

- [x] Leaving the atmosphere (Closed Beta) - may still contain bugs.

- [ ] Orbit (general availability) - ready for scale and production use cases.


You can see the current state of our progress in our [Roadmap](https://github.com/orgs/Airbotics/projects/1).



Development has paused and it should not be considered ready for production usage.



Expand All @@ -159,15 +77,10 @@ You can see the current state of our progress in our [Roadmap](https://github.co



## Community

- [Discord](https://discord.gg/W2TR4WXUqv) - for hanging out with people building and using Airbotics.

- [GitHub Issues](https://github.com/Airbotics/airbotics/issues) - for bugs and feature requests.

- [Twitter](https://twitter.com/Airboticsio) - for updates and announcements.

- [Email](mailto:hello@airbotics.io) - for contacting the maintainers directly.



Expand Down
23 changes: 11 additions & 12 deletions src/modules/admin/provisioning-credentials/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,11 @@ export const downloadProvisioningCredential = async (req: Request, res: Response
return new BadResponse(res, 'Could not create provisioning credentials.');
}

// const clientCert = await certificateManager.downloadCertificate(teamID, provisioningCredentials.client_cert_id);
const clientCert = await certificateManager.downloadCertificate(teamID, provisioningCredentials.client_cert_id);

// if(!clientCert) {
// return new BadResponse(res, 'Could not create provisioning credentials.');
// }
if(!clientCert) {
return new BadResponse(res, 'Could not create provisioning credentials.');
}

// get provisioning and client key pairs
const provisioningKeyPair = await keyStorage.getKeyPair(provisioningCredentials.provisioning_cert_id);
Expand All @@ -147,10 +147,10 @@ export const downloadProvisioningCredential = async (req: Request, res: Response
null,
{ algorithm: 'aes256' });

// const clientp12 = forge.pkcs12.toPkcs12Asn1(forge.pki.privateKeyFromPem(clientKeyPair.privateKey),
// [forge.pki.certificateFromPem(clientCert), forge.pki.certificateFromPem(rootCACertStr)],
// null,
// { algorithm: 'aes256' });
const clientp12 = forge.pkcs12.toPkcs12Asn1(forge.pki.privateKeyFromPem(clientKeyPair.privateKey),
[forge.pki.certificateFromPem(clientCert), forge.pki.certificateFromPem(rootCACertStr)],
null,
{ algorithm: 'aes256' });

// get initial root metadata from image repo
const rootMetadata = await getTufMetadata(teamID, TUFRepo.image, TUFRole.root, TUF_METADATA_INITIAL);
Expand All @@ -168,8 +168,7 @@ export const downloadProvisioningCredential = async (req: Request, res: Response
const treehubJson = {
no_auth: true,
ostree: {
// server: `${config.GATEWAY_ORIGIN}/api/v0/robot/treehub`
server: `${config.API_ORIGIN}/api/v0/robot/treehub/${teamID}`
server: `${config.GATEWAY_ORIGIN}/api/v0/robot/treehub`
}
};

Expand All @@ -181,12 +180,12 @@ export const downloadProvisioningCredential = async (req: Request, res: Response
const archive = archiver('zip');
archive.append(Buffer.from(toCanonical(targetsTufKeyPublic), 'ascii'), { name: 'targets.pub' });
archive.append(Buffer.from(toCanonical(targetsTufKeyPrivate), 'ascii'), { name: 'targets.sec' });
// archive.append(Buffer.from(`${config.GATEWAY_ORIGIN}/api/v0/robot/repo`, 'ascii'), { name: 'tufrepo.url' });
archive.append(Buffer.from(`${config.GATEWAY_ORIGIN}/api/v0/robot/repo`, 'ascii'), { name: 'tufrepo.url' });
archive.append(Buffer.from(`${config.API_ORIGIN}/api/v0/robot/repo/${teamID}`, 'ascii'), { name: 'tufrepo.url' });
archive.append(Buffer.from(toCanonical(rootMetadata), 'ascii'), { name: 'root.json' });
archive.append(Buffer.from(JSON.stringify(treehubJson), 'ascii'), { name: 'treehub.json' });
archive.append(Buffer.from(`${config.GATEWAY_ORIGIN}/api/v0/robot`, 'ascii'), { name: 'autoprov.url' });
// archive.append(Buffer.from(forge.asn1.toDer(clientp12).getBytes(), 'binary'), { name: 'client_auth.p12' });
archive.append(Buffer.from(forge.asn1.toDer(clientp12).getBytes(), 'binary'), { name: 'client_auth.p12' });
archive.append(Buffer.from(forge.asn1.toDer(provisioningp12).getBytes(), 'binary'), { name: 'autoprov_credentials.p12' });
archive.finalize();

Expand Down
3 changes: 1 addition & 2 deletions src/modules/treehub/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const router = express.Router();

const getConfig = async (req: Request, res: Response) => {

const team_id = req.params.team_id || req.robotGatewayPayload!.team_id;
const team_id = req.robotGatewayPayload!.team_id;

const teamCount = await prisma.team.count({
where: {
Expand All @@ -36,7 +36,6 @@ const getConfig = async (req: Request, res: Response) => {
*/
router.get('/config', mustBeRobot, updateRobotMeta, getConfig);

router.get('/:team_id/config', getConfig);


export default router;
20 changes: 5 additions & 15 deletions src/modules/treehub/objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const router = express.Router();

const downloadObject = async (req: Request, res: Response) => {

const team_id = req.params.team_id || req.robotGatewayPayload!.team_id;
const team_id = req.robotGatewayPayload!.team_id;

const prefix = req.params.prefix;
const suffix = req.params.suffix;
Expand Down Expand Up @@ -66,10 +66,9 @@ const downloadObject = async (req: Request, res: Response) => {
*
* Will store in s3 or local filesystem depending on config.
*/
router.post('/:team_id/objects/:prefix/:suffix', express.raw({ type: '*/*', limit: config.MAX_TREEHUB_REQUEST_SIZE }), async (req: Request, res) => {
router.post('/objects/:prefix/:suffix', express.raw({ type: '*/*', limit: config.MAX_TREEHUB_REQUEST_SIZE }), async (req: Request, res) => {

// const teamID = req.robotGatewayPayload!.team_id;
const teamID = req.params.team_id;
const teamID = req.robotGatewayPayload!.team_id;
const prefix = req.params.prefix;
const suffix = req.params.suffix;
const content = req.body;
Expand Down Expand Up @@ -144,10 +143,9 @@ router.post('/:team_id/objects/:prefix/:suffix', express.raw({ type: '*/*', limi
* Note: this does not directly interface with blob storage, instead it checks
* the record of it in Postgres. This assumes they are in sync.
*/
router.head('/:team_id/objects/:prefix/:suffix', async (req: Request, res) => {
router.head('/objects/:prefix/:suffix', async (req: Request, res) => {

// const teamID = req.robotGatewayPayload!.team_id;
const teamID = req.params.team_id;
const teamID = req.robotGatewayPayload!.team_id;
const prefix = req.params.prefix;
const suffix = req.params.suffix;
const object_id = prefix + suffix;
Expand All @@ -171,14 +169,6 @@ router.head('/:team_id/objects/:prefix/:suffix', async (req: Request, res) => {
});


/**
* Gets an object from blob storage.
*
* Will fetch from s3 or local filesystem depending on config.
*/
router.get('/:team_id/objects/:prefix/:suffix', downloadObject);


/**
* Gets an object from blob storage.
*
Expand Down
13 changes: 5 additions & 8 deletions src/modules/treehub/refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,9 @@ const getRef = async (req: Request, res: Response) => {


// create a ref
router.post('/:team_id/refs/heads/:name', express.text({ type: '*/*', limit: config.MAX_TREEHUB_REQUEST_SIZE}), async (req: Request, res: Response) => {
router.post('/refs/heads/:name', express.text({ type: '*/*', limit: config.MAX_TREEHUB_REQUEST_SIZE}), async (req: Request, res: Response) => {

// const teamID = req.headers['air-team-id']!;
const team_id = req.params.team_id;
const teamID = req.headers['air-team-id']!;
const name = req.params.name;

const commit = req.body;
Expand All @@ -48,7 +47,7 @@ router.post('/:team_id/refs/heads/:name', express.text({ type: '*/*', limit: con
// check team exists
const teamCount = await prisma.team.count({
where: {
id: team_id
id: teamID
}
});

Expand All @@ -71,7 +70,7 @@ router.post('/:team_id/refs/heads/:name', express.text({ type: '*/*', limit: con

await prisma.ref.upsert({
create: {
team_id: team_id,
team_id: teamID,
name,
object_id,
commit
Expand All @@ -82,7 +81,7 @@ router.post('/:team_id/refs/heads/:name', express.text({ type: '*/*', limit: con
},
where: {
team_id_name: {
team_id: team_id,
team_id: teamID,
name
}
}
Expand All @@ -92,8 +91,6 @@ router.post('/:team_id/refs/heads/:name', express.text({ type: '*/*', limit: con
return res.status(200).end();
});

// get a ref
router.get('/:team_id/refs/heads/:name', getRef);

// get a ref
router.get('/refs/heads/:name', mustBeRobot, updateRobotMeta, getRef);
Expand Down
12 changes: 5 additions & 7 deletions src/modules/treehub/summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const router = express.Router();

const downloadSummary = async (req: Request, res: Response) => {

const team_id = req.params.team_id || req.robotGatewayPayload!.team_id;
const team_id = req.robotGatewayPayload!.team_id;

try {
const content = await blobStorage.getObject(config.TREEHUB_BUCKET_NAME!, team_id, 'summary');
Expand All @@ -36,9 +36,9 @@ const downloadSummary = async (req: Request, res: Response) => {
* - check size in header matches size of request body.
* - restrict allowable mime-types
*/
router.put('/:team_id/summary', express.raw({ type: '*/*', limit: config.MAX_TREEHUB_REQUEST_SIZE }), async (req: Request, res: Response) => {
router.put('/summary', express.raw({ type: '*/*', limit: config.MAX_TREEHUB_REQUEST_SIZE }), async (req: Request, res: Response) => {

const teamID = req.params.team_id;
const teamId = req.robotGatewayPayload!.team_id;
const content = req.body;

const size = parseInt(req.get('content-length')!);
Expand All @@ -51,7 +51,7 @@ router.put('/:team_id/summary', express.raw({ type: '*/*', limit: config.MAX_TRE

const teamCount = await prisma.team.count({
where: {
id: teamID
id: teamId
}
});

Expand All @@ -60,15 +60,13 @@ router.put('/:team_id/summary', express.raw({ type: '*/*', limit: config.MAX_TRE
return res.status(400).end();
}

await blobStorage.putObject(config.TREEHUB_BUCKET_NAME!, teamID, 'summary', content);
await blobStorage.putObject(config.TREEHUB_BUCKET_NAME!, teamId, 'summary', content);

logger.info('uploaded ostree summary');
return res.status(200).end();

});

// download summary
router.get('/:team_id/summary', downloadSummary);

// download summary
router.get('/summary', mustBeRobot, updateRobotMeta, downloadSummary);
Expand Down

0 comments on commit 7c95197

Please sign in to comment.