Auto-merge for PR #547 via VersionBot

Implement an API for proxy and hostname configuration, and centralize…
This commit is contained in:
resin-io-versionbot[bot] 2018-01-18 22:28:24 +00:00 committed by GitHub
commit 7e04b4aec0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 483 additions and 126 deletions

View File

@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
This project adheres to [Semantic Versioning](http://semver.org/).
## v6.6.0 - 2018-01-18
* Implement an API for proxy and hostname configuration, and centralize management of config.json #547 [Pablo Carranza Velez]
## v6.5.9 - 2018-01-12
* Fix saving deviceApiKey to the DB (to fix the RESIN_API_KEY env var) when updating from some older supervisors #544 [Pablo Carranza Velez]

View File

@ -14,7 +14,7 @@ The supervisor exposes an HTTP API on port 48484 (`RESIN_SUPERVISOR_PORT`).
The full address for the API, i.e. `"http://127.0.0.1:48484"`, is available as `RESIN_SUPERVISOR_ADDRESS`. **Always use these variables when communicating via the API, since address and port could change**.
Alternatively, the Resin API (api.resin.io) has a proxy endpoint at `POST /supervisor/<url>` (where `<url>` is one of the API URLs described below) from which you can send API commands to the supervisor remotely, using your Auth Token instead of your API key. Commands sent through the proxy require an `appId` and/or `deviceId` parameter in the body, and default to POST requests unless you specify a `method` parameter (e.g. "GET").
Alternatively, the Resin API (api.resin.io) has a proxy endpoint at `POST /supervisor/<url>` (where `<url>` is one of the API URLs described below) from which you can send API commands to the supervisor remotely, using your Auth Token instead of your API key. Commands sent through the proxy can specify either an `appId` to send the request to all devices in an application, or a `deviceId` or `uuid` to send to a particular device. These requests default to POST unless you specify a `method` parameter (e.g. "GET"). In the examples below, we show how to use a uuid to specify a device, but in any of those you can replace `uuid` for a `deviceId` or `appId`.
The API is versioned (currently at v1), except for `/ping`.
@ -43,7 +43,7 @@ Remotely via the API proxy:
```bash
$ curl -X POST --header "Content-Type:application/json" \
--header "Authorization: Bearer <auth token>" \
--data '{"deviceId": <deviceId>, "appId": <appId>, "method": "GET"}' \
--data '{"uuid": <uuid>, "method": "GET"}' \
"https://api.resin.io/supervisor/ping"
```
@ -67,7 +67,7 @@ Remotely via the API proxy:
```bash
$ curl -X POST --header "Content-Type:application/json" \
--header "Authorization: Bearer <auth token>" \
--data '{"deviceId": <deviceId>, "appId": <appId>}' \
--data '{"uuid": <uuid>}' \
"https://api.resin.io/supervisor/v1/blink"
```
@ -100,7 +100,7 @@ Remotely via the API proxy:
```bash
$ curl -X POST --header "Content-Type:application/json" \
--header "Authorization: Bearer <auth token>" \
--data '{"deviceId": <deviceId>, "appId": <appId>, "data": {"force": true}}' \
--data '{"uuid": <uuid>, "data": {"force": true}}' \
"https://api.resin.io/supervisor/v1/update"
```
@ -134,7 +134,7 @@ Remotely via the API proxy:
```bash
$ curl -X POST --header "Content-Type:application/json" \
--header "Authorization: Bearer <auth token>" \
--data '{"deviceId": <deviceId>, "appId": <appId>}' \
--data '{"uuid": <uuid>}' \
"https://api.resin.io/supervisor/v1/reboot"
```
@ -169,7 +169,7 @@ Remotely via the API proxy:
```bash
$ curl -X POST --header "Content-Type:application/json" \
--header "Authorization: Bearer <auth token>" \
--data '{"deviceId": <deviceId>, "appId": <appId>}' \
--data '{"uuid": <uuid>}' \
"https://api.resin.io/supervisor/v1/shutdown"
```
@ -217,7 +217,7 @@ Remotely via the API proxy:
```bash
$ curl -X POST --header "Content-Type:application/json" \
--header "Authorization: Bearer <auth token>" \
--data '{"deviceId": <deviceId>, "appId": <appId>, "data": {"appId": <appId>}}' \
--data '{"uuid": <uuid>, "data": {"appId": <appId>}}' \
"https://api.resin.io/supervisor/v1/purge"
```
@ -260,7 +260,7 @@ Remotely via the API proxy:
```bash
$ curl -X POST --header "Content-Type:application/json" \
--header "Authorization: Bearer <auth token>" \
--data '{"deviceId": <deviceId>, "appId": <appId>, "data": {"appId": <appId>}}' \
--data '{"uuid": <uuid>, "data": {"appId": <appId>}}' \
"https://api.resin.io/supervisor/v1/restart"
```
@ -285,7 +285,7 @@ Remotely via the API proxy:
```bash
$ curl -X POST --header "Content-Type:application/json" \
--header "Authorization: Bearer <auth token>" \
--data '{"deviceId": <deviceId>, "appId": <appId>}' \
--data '{"uuid": <uuid>}' \
"https://api.resin.io/supervisor/v1/tcp-ping"
```
@ -310,7 +310,7 @@ Remotely via the API proxy:
```bash
$ curl -X POST --header "Content-Type:application/json" \
--header "Authorization: Bearer <auth token>" \
--data '{"deviceId": <deviceId>, "appId": <appId>, "method": "DELETE"}' \
--data '{"uuid": <uuid>, "method": "DELETE"}' \
"https://api.resin.io/supervisor/v1/tcp-ping"
```
@ -335,7 +335,7 @@ Remotely via the API proxy:
```bash
$ curl -X POST --header "Content-Type:application/json" \
--header "Authorization: Bearer <auth token>" \
--data '{"deviceId": <deviceId>, "appId": <appId>}' \
--data '{"uuid": <uuid>}' \
"https://api.resin.io/supervisor/v1/regenerate-api-key"
```
@ -374,7 +374,7 @@ Remotely via the API proxy:
```bash
$ curl -X POST --header "Content-Type:application/json" \
--header "Authorization: Bearer <auth token>" \
--data '{"deviceId": <deviceId>, "appId": <appId>, "method": "GET"}' \
--data '{"uuid": <uuid>, "method": "GET"}' \
"https://api.resin.io/supervisor/v1/device"
```
@ -412,7 +412,7 @@ Remotely via the API proxy:
```bash
$ curl -X POST --header "Content-Type:application/json" \
--header "Authorization: Bearer <auth token>" \
--data '{"deviceId": <deviceId>, "appId": <appId>}' \
--data '{"uuid": <uuid>}' \
"https://api.resin.io/supervisor/v1/apps/<appId>/stop"
```
@ -446,7 +446,7 @@ Remotely via the API proxy:
```bash
$ curl -X POST --header "Content-Type:application/json" \
--header "Authorization: Bearer <auth token>" \
--data '{"deviceId": <deviceId>, "appId": <appId>}' \
--data '{"uuid": <uuid>}' \
"https://api.resin.io/supervisor/v1/apps/<appId>/start"
```
@ -480,7 +480,7 @@ Remotely via the API proxy:
```bash
$ curl -X POST --header "Content-Type:application/json" \
--header "Authorization: Bearer <auth token>" \
--data '{"deviceId": <deviceId>, "appId": <appId>, "method": "GET"}' \
--data '{"uuid": <uuid>, "method": "GET"}' \
"https://api.resin.io/supervisor/v1/apps/<appId>"
```
@ -507,6 +507,101 @@ Remotely via the API proxy:
```bash
$ curl -X POST --header "Content-Type:application/json" \
--header "Authorization: Bearer <auth token>" \
--data '{"deviceId": <deviceId>, "appId": <appId>, "method": "GET"}' \
--data '{"uuid": <uuid>, "method": "GET"}' \
"https://api.resin.io/supervisor/v1/healthy"
```
<hr>
### PATCH /v1/device/host-config
Added in supervisor v6.6.0.
This endpoint allows setting some configuration values for the host OS. Currently it supports
proxy and hostname configuration.
For proxy configuration, resinOS 2.0.7 and higher provides a transparent proxy redirector (redsocks) that makes all connections be routed to a SOCKS or HTTP proxy. This endpoint allows user applications to modify these proxy settings at runtime.
#### Request body
Is a JSON object with several optional fields. Proxy and hostname configuration go under a "network" key. If "proxy" or "hostname" are not present (undefined), those values will not be modified, so that a request can modify hostname
without changing proxy settings and viceversa.
```json
{
"network": {
"proxy": {
"type": "http-connect",
"ip": "myproxy.example.com",
"port": 8123,
"login": "username",
"password": "password",
"noProxy": [ "152.10.30.4", "253.1.1.0/16" ]
},
"hostname": "mynewhostname"
}
}
```
In the proxy settings, `type`, `ip`, `port`, `login` and `password` are the settings for the proxy redirector to
be able to connnect to the proxy, based on how [redsocks.conf](https://github.com/darkk/redsocks/blob/master/redsocks.conf.example) works. `type` can be `socks4`, `socks5`, `http-connect` or `http-relay` (not all proxies are
guaranteed to work, especially if they block connections that the resin services may require).
Keep in mind that, even if transparent proxy redirection will take effect immediately after the API call (i.e. all new connections will go through the proxy), open connections will not be closed. So, if for example, the device has managed to connect to the resin VPN without the proxy, it will stay connected directly without trying to reconnect through the proxy, unless the connection breaks - any reconnection attempts will then go through the proxy. To force *all* connections to go through the proxy, the best way is to reboot the device (see the /v1/reboot endpoint). In most networks were no connections to the Internet can be made if not through a proxy, this should not be necessary (as there will be no open connections before configuring the proxy settings).
The "noProxy" setting for the proxy is an optional array of IP addresses/subnets that should not be routed through the
proxy. Keep in mind that local/reserved subnets are already [excluded by resinOS automatically](https://github.com/resin-os/meta-resin/blob/master/meta-resin-common/recipes-connectivity/resin-proxy-config/resin-proxy-config/resin-proxy-config#L48).
If either "proxy" or "hostname" are null or empty values (i.e. `{}` for proxy or an empty string for hostname), they will be cleared to their default values (i.e. not using a proxy, and a hostname equal to the first 7 characters of the device's uuid, respectively).
#### Examples:
From the app on the device:
```bash
$ curl -X PATCH --header "Content-Type:application/json" \
--data '{"network": {"hostname": "newhostname"}}' \
"$RESIN_SUPERVISOR_ADDRESS/v1/device/host-config?apikey=$RESIN_SUPERVISOR_API_KEY"
```
Response:
```none
OK
```
Remotely via the API proxy:
```bash
$ curl -X POST --header "Content-Type:application/json" \
--header "Authorization: Bearer <auth token>" \
--data '{"uuid": <uuid>, "method": "PATCH", "data": {"network": {"hostname": "newhostname"}}}' \
"https://api.resin.io/supervisor/v1/device/host-config"
```
<hr>
### GET /v1/device/host-config
Added in supervisor v6.6.0.
This endpoint allows reading some configuration values for the host OS, previously set with `PATCH /v1/device/host-config`. Currently it supports
proxy and hostname configuration.
Please refer to the PATCH endpoint above for details on the behavior and meaning of the fields in the response.
#### Examples:
From the app on the device:
```bash
$ curl "$RESIN_SUPERVISOR_ADDRESS/v1/device/host-config?apikey=$RESIN_SUPERVISOR_API_KEY"
```
Response:
```json
{"network":{"proxy":{"ip":"192.168.0.199","port":"8123","type":"socks5"},"hostname":"27b0fdc"}}
```
Remotely via the API proxy:
```bash
$ curl -X POST --header "Content-Type:application/json" \
--header "Authorization: Bearer <auth token>" \
--data '{"uuid": <uuid>, "method": "GET"}' \
"https://api.resin.io/supervisor/v1/device/host-config"
```

View File

@ -38,6 +38,10 @@ type LogToDisplayBody struct {
Enable bool
}
type RestartServiceBody struct {
Name string
}
func jsonResponse(writer http.ResponseWriter, response interface{}, status int) {
jsonBody, err := json.Marshal(response)
if err != nil {
@ -278,3 +282,29 @@ func LogToDisplayControl(writer http.ResponseWriter, request *http.Request) {
sendResponse(true, "", http.StatusOK)
}
}
//RestartService is used to restart a systemd service by name
func RestartService(writer http.ResponseWriter, request *http.Request) {
sendResponse, sendError := responseSenders(writer)
var body RestartServiceBody
if err := parseJSONBody(&body, request); err != nil {
log.Println(err)
sendResponse("Error", err.Error(), http.StatusBadRequest)
return
}
if systemd.Dbus == nil {
sendError(fmt.Errorf("Systemd dbus unavailable, cannot restart service."))
return
}
actionDescr := "Service Restart"
if _, err := systemd.Dbus.RestartUnit(body.Name+".service", "fail", nil); err != nil {
log.Printf("%s: %s\n", actionDescr, err)
sendError(err)
return
}
log.Printf("%sd\n", actionDescr)
sendResponse("OK", "", http.StatusOK)
}

View File

@ -26,6 +26,7 @@ func setupApi(router *mux.Router) {
apiv1.HandleFunc("/shutdown", ShutdownHandler).Methods("POST")
apiv1.HandleFunc("/vpncontrol", VPNControl).Methods("POST")
apiv1.HandleFunc("/set-log-to-display", LogToDisplayControl).Methods("POST")
apiv1.HandleFunc("/restart-service", RestartService).Methods("POST")
}
func startApi(listenAddress string, router *mux.Router) {

View File

@ -1,7 +1,7 @@
{
"name": "resin-supervisor",
"description": "This is resin.io's Supervisor, a program that runs on IoT devices and has the task of running user Apps (which are Docker containers), and updating them as Resin's API informs it to.",
"version": "6.5.9",
"version": "6.6.0",
"license": "Apache-2.0",
"repository": {
"type": "git",
@ -40,6 +40,7 @@
"log-timestamp": "^0.1.2",
"memoizee": "^0.4.1",
"mixpanel": "0.0.20",
"mkdirp": "^0.5.1",
"network-checker": "~0.0.5",
"node-loader": "^0.6.0",
"null-loader": "^0.1.1",

View File

@ -7,6 +7,7 @@ config = require './config'
device = require './device'
_ = require 'lodash'
proxyvisor = require './proxyvisor'
hostConfig = require './host-config'
module.exports = (application) ->
authenticate = (req, res, next) ->
@ -208,7 +209,6 @@ module.exports = (application) ->
.catch (err) ->
res.status(503).send(err?.message or err or 'Unknown error')
# Expires the supervisor's API key and generates a new one.
# It also communicates the new key to the Resin API.
unparsedRouter.post '/v1/regenerate-api-key', (req, res) ->
@ -219,6 +219,20 @@ module.exports = (application) ->
.catch (err) ->
res.status(503).send(err?.message or err or 'Unknown error')
parsedRouter.get '/v1/device/host-config', (req, res) ->
hostConfig.get()
.then (conf) ->
res.json(conf)
.catch (err) ->
res.status(503).send(err?.message or err or 'Unknown error')
parsedRouter.patch '/v1/device/host-config', (req, res) ->
hostConfig.patch(req.body)
.then ->
res.status(200).send('OK')
.catch (err) ->
res.status(503).send(err?.message or err or 'Unknown error')
unparsedRouter.get '/v1/device', (req, res) ->
res.json(device.getState())

View File

@ -5,7 +5,6 @@ deviceRegister = require 'resin-register-device'
{ resinApi, request } = require './request'
fs = Promise.promisifyAll(require('fs'))
config = require './config'
configPath = '/boot/config.json'
appsPath = '/boot/apps.json'
_ = require 'lodash'
deviceConfig = require './device-config'
@ -13,23 +12,13 @@ TypedError = require 'typed-error'
osRelease = require './lib/os-release'
semver = require 'semver'
semverRegex = require('semver-regex')
userConfig = {}
configJson = require './config-json'
DuplicateUuidError = (err) -> _.startsWith(err.message, '"uuid" must be unique')
exports.ExchangeKeyError = class ExchangeKeyError extends TypedError
bootstrapper = {}
writeAndSyncFile = (path, data) ->
fs.openAsync(path, 'w')
.then (fd) ->
fs.writeAsync(fd, data, 0, 'utf8')
.then ->
fs.fsyncAsync(fd)
.then ->
fs.closeAsync(fd)
loadPreloadedApps = ->
devConfig = {}
knex('app').select()
@ -37,27 +26,29 @@ loadPreloadedApps = ->
if apps.length > 0
console.log('Preloaded apps already loaded, skipping')
return
fs.readFileAsync(appsPath, 'utf8')
.then(JSON.parse)
.map (app) ->
utils.extendEnvVars(app.env, userConfig.uuid, userConfig.deviceApiKey, app.appId, app.name, app.commit)
.then (extendedEnv) ->
app.env = JSON.stringify(extendedEnv)
app.markedForDeletion = false
_.merge(devConfig, app.config)
app.config = JSON.stringify(app.config)
knex('app').insert(app)
.then ->
deviceConfig.set({ targetValues: devConfig })
configJson.getAll()
.then (userConfig) ->
fs.readFileAsync(appsPath, 'utf8')
.then(JSON.parse)
.map (app) ->
utils.extendEnvVars(app.env, userConfig.uuid, userConfig.deviceApiKey, app.appId, app.name, app.commit)
.then (extendedEnv) ->
app.env = JSON.stringify(extendedEnv)
app.markedForDeletion = false
_.merge(devConfig, app.config)
app.config = JSON.stringify(app.config)
knex('app').insert(app)
.then ->
deviceConfig.set({ targetValues: devConfig })
.catch (err) ->
utils.mixpanelTrack('Loading preloaded apps failed', { error: err })
fetchDevice = (apiKey) ->
fetchDevice = (uuid, apiKey) ->
resinApi.get
resource: 'device'
options:
filter:
uuid: userConfig.uuid
uuid: uuid
customOptions:
apikey: apiKey
.get(0)
@ -65,33 +56,47 @@ fetchDevice = (apiKey) ->
.timeout(config.apiTimeout)
exchangeKey = ->
Promise.try ->
# If we have an existing device key we first check if it's valid, because if it is we can just use that
if userConfig.deviceApiKey?
fetchDevice(userConfig.deviceApiKey)
.then (device) ->
if device?
return device
# If it's not valid/doesn't exist then we try to use the user/provisioning api key for the exchange
fetchDevice(userConfig.apiKey)
configJson.getAll()
.then (userConfig) ->
Promise.try ->
# If we have an existing device key we first check if it's valid, because if it is we can just use that
if userConfig.deviceApiKey?
fetchDevice(userConfig.uuid, userConfig.deviceApiKey)
.then (device) ->
if not device?
throw new ExchangeKeyError("Couldn't fetch device with provisioning key")
# We found the device, we can try to register a working device key for it
userConfig.deviceApiKey ?= deviceRegister.generateUniqueKey()
request.postAsync("#{config.apiEndpoint}/api-key/device/#{device.id}/device-key?apikey=#{userConfig.apiKey}", {
json: true
body:
apiKey: userConfig.deviceApiKey
})
.spread (res, body) ->
if res.statusCode != 200
throw new ExchangeKeyError("Couldn't register device key with provisioning key")
.return(device)
if device?
return device
# If it's not valid/doesn't exist then we try to use the user/provisioning api key for the exchange
fetchDevice(userConfig.uuid, userConfig.apiKey)
.then (device) ->
if not device?
throw new ExchangeKeyError("Couldn't fetch device with provisioning key")
# We found the device, we can try to register a working device key for it
Promise.try ->
if !userConfig.deviceApiKey?
deviceApiKey = deviceRegister.generateUniqueKey()
configJson.set({ deviceApiKey })
.return(deviceApiKey)
else
return userConfig.deviceApiKey
.then (deviceApiKey) ->
request.postAsync("#{config.apiEndpoint}/api-key/device/#{device.id}/device-key?apikey=#{userConfig.apiKey}", {
json: true
body:
apiKey: deviceApiKey
})
.spread (res, body) ->
if res.statusCode != 200
throw new ExchangeKeyError("Couldn't register device key with provisioning key")
.return(device)
bootstrap = ->
Promise.try ->
userConfig.deviceType ?= 'raspberry-pi'
configJson.get('deviceType')
.then (deviceType) ->
if !deviceType?
configJson.set(deviceType: 'raspberry-pi')
.then ->
configJson.getAll()
.then (userConfig) ->
if userConfig.registered_at?
return userConfig
@ -115,18 +120,22 @@ bootstrap = ->
console.log('Exchanging key failed, having to reregister')
generateRegistration(true)
.then ({ id }) ->
userConfig.registered_at = Date.now()
userConfig.deviceId = id
toUpdate = {}
toDelete = []
if !userConfig.registered_at?
toUpdate.registered_at = Date.now()
toUpdate.deviceId = id
osRelease.getOSVersion(config.hostOSVersionPath)
.then (osVersion) ->
# Delete the provisioning key now, only if the OS supports it
hasSupport = hasDeviceApiKeySupport(osVersion)
if hasSupport
delete userConfig.apiKey
else
userConfig.apiKey = userConfig.deviceApiKey
writeAndSyncFile(configPath, JSON.stringify(userConfig))
.return(userConfig)
.then (osVersion) ->
# Delete the provisioning key now, only if the OS supports it
hasSupport = hasDeviceApiKeySupport(osVersion)
if hasSupport
toDelete.push('apiKey')
else
toUpdate.apiKey = userConfig.deviceApiKey
configJson.set(toUpdate, toDelete)
.then ->
configJson.getAll()
.then (userConfig) ->
console.log('Finishing bootstrapping')
knex('config').whereIn('key', ['uuid', 'apiKey', 'username', 'userId', 'version']).delete()
@ -144,20 +153,18 @@ bootstrap = ->
.tap ->
bootstrapper.doneBootstrapping()
readConfig = ->
fs.readFileAsync(configPath, 'utf8')
.then(JSON.parse)
generateRegistration = (forceReregister = false) ->
Promise.try ->
if forceReregister
userConfig.uuid = deviceRegister.generateUniqueKey()
userConfig.deviceApiKey = deviceRegister.generateUniqueKey()
configJson.set({ uuid: deviceRegister.generateUniqueKey(), deviceApiKey: deviceRegister.generateUniqueKey() })
else
userConfig.uuid ?= deviceRegister.generateUniqueKey()
userConfig.deviceApiKey ?= deviceRegister.generateUniqueKey()
writeAndSyncFile(configPath, JSON.stringify(userConfig))
.return(userConfig.uuid)
configJson.getAll()
.then ({ uuid, deviceApiKey }) ->
uuid ?= deviceRegister.generateUniqueKey()
deviceApiKey ?= deviceRegister.generateUniqueKey()
configJson.set({ uuid, deviceApiKey })
.then ->
configJson.get('uuid')
.catch (err) ->
console.log('Error generating and saving UUID: ', err)
Promise.delay(config.bootstrapRetryDelay)
@ -186,23 +193,27 @@ exchangeKeyAndUpdateConfig = ->
# Otherwise VPN and other host services that use an API key will break.
#
# In other cases, we make the apiKey equal the deviceApiKey instead.
osRelease.getOSVersion(config.hostOSVersionPath)
.then (osVersion) ->
hasSupport = hasDeviceApiKeySupport(osVersion)
if hasSupport or userConfig.apiKey != userConfig.deviceApiKey
console.log('Attempting key exchange')
exchangeKey()
.then ->
console.log('Key exchange succeeded, starting to use deviceApiKey')
if hasSupport
delete userConfig.apiKey
else
userConfig.apiKey = userConfig.deviceApiKey
utils.setConfig('deviceApiKey', userConfig.deviceApiKey)
.then ->
utils.setConfig('apiKey', userConfig.deviceApiKey)
.then ->
writeAndSyncFile(configPath, JSON.stringify(userConfig))
Promise.join(
configJson.getAll()
osRelease.getOSVersion(config.hostOSVersionPath)
(userConfig, osVersion) ->
hasSupport = hasDeviceApiKeySupport(osVersion)
if hasSupport or userConfig.apiKey != userConfig.deviceApiKey
console.log('Attempting key exchange')
exchangeKey()
.then ->
configJson.get('deviceApiKey')
.then (deviceApiKey) ->
console.log('Key exchange succeeded, starting to use deviceApiKey')
utils.setConfig('deviceApiKey', deviceApiKey)
.then ->
utils.setConfig('apiKey', deviceApiKey)
.then ->
if hasSupport
configJson.set({}, [ 'apiKey' ])
else
configJson.set(apiKey: deviceApiKey)
)
exchangeKeyOrRetry = do ->
_failedExchanges = 0
@ -217,31 +228,34 @@ exchangeKeyOrRetry = do ->
bootstrapper.done = new Promise (resolve) ->
bootstrapper.doneBootstrapping = ->
bootstrapper.bootstrapped = true
resolve(userConfig)
# If we're still using an old api key we can try to exchange it for a valid device key
# This will only be the case when the supervisor/OS has been updated.
if userConfig.apiKey?
exchangeKeyOrRetry()
else
Promise.join(
knex('config').select('value').where(key: 'apiKey')
knex('config').select('value').where(key: 'deviceApiKey')
([ apiKey ], [ deviceApiKey ]) ->
if !deviceApiKey?.value
# apiKey in the DB is actually the deviceApiKey, but it was
# exchanged in a supervisor version that didn't save it to the DB
# (which mainly affects the RESIN_API_KEY env var)
knex('config').insert({ key: 'deviceApiKey', value: apiKey.value })
)
configJson.getAll()
.then (userConfig) ->
bootstrapper.bootstrapped = true
resolve(userConfig)
# If we're still using an old api key we can try to exchange it for a valid device key
# This will only be the case when the supervisor/OS has been updated.
if userConfig.apiKey?
exchangeKeyOrRetry()
else
Promise.join(
knex('config').select('value').where(key: 'apiKey')
knex('config').select('value').where(key: 'deviceApiKey')
([ apiKey ], [ deviceApiKey ]) ->
if !deviceApiKey?.value
# apiKey in the DB is actually the deviceApiKey, but it was
# exchanged in a supervisor version that didn't save it to the DB
# (which mainly affects the RESIN_API_KEY env var)
knex('config').insert({ key: 'deviceApiKey', value: apiKey.value })
)
return
bootstrapper.bootstrapped = false
bootstrapper.startBootstrapping = ->
# Load config file
readConfig()
.then (configFromFile) ->
userConfig = configFromFile
configJson.init()
.then ->
configJson.getAll()
.then (userConfig) ->
bootstrapper.offlineMode = !Boolean(config.apiEndpoint) or Boolean(userConfig.supervisorOfflineMode)
knex('config').select('value').where(key: 'uuid')
.then ([ uuid ]) ->

46
src/config-json.coffee Normal file
View File

@ -0,0 +1,46 @@
Promise = require 'bluebird'
_ = require 'lodash'
configPath = '/boot/config.json'
Lock = require 'rwlock'
fs = Promise.promisifyAll(require('fs'))
{ writeAndSyncFile } = require './lib/fs-utils'
lock = new Lock()
writeLock = Promise.promisify(lock.async.writeLock)
readLock = Promise.promisify(lock.async.readLock)
withWriteLock = do ->
_takeLock = ->
writeLock('config')
.disposer (release) ->
release()
return (func) ->
Promise.using(_takeLock(), func)
withReadLock = do ->
_takeLock = ->
readLock('config')
.disposer (release) ->
release()
return (func) ->
Promise.using(_takeLock(), func)
# write-through cache of the config.json file
userConfig = null
exports.init = ->
fs.readFileAsync(configPath)
.then (conf) ->
userConfig = JSON.parse(conf)
exports.get = (key) ->
withReadLock ->
return userConfig[key]
exports.getAll = ->
withReadLock ->
return _.clone(userConfig)
exports.set = (vals = {}, keysToDelete = []) ->
withWriteLock ->
_.merge(userConfig, vals)
for key in keysToDelete
delete userConfig[key]
writeAndSyncFile(configPath, JSON.stringify(userConfig))

128
src/host-config.coffee Normal file
View File

@ -0,0 +1,128 @@
Promise = require 'bluebird'
_ = require 'lodash'
utils = require './utils'
path = require 'path'
config = require './config'
fs = Promise.promisifyAll(require('fs'))
configJson = require './config-json'
{ writeFileAtomic } = require './lib/fs-utils'
mkdirp = Promise.promisify(require('mkdirp'))
ENOENT = (err) -> err.code is 'ENOENT'
redsocksHeader = '''
base {
log_debug = off;
log_info = on;
log = stderr;
daemon = off;
redirector = iptables;
}
redsocks {
local_ip = 127.0.0.1;
local_port = 12345;
'''
redsocksFooter = '}\n'
proxyFields = [ 'type', 'ip', 'port', 'login', 'password' ]
proxyBasePath = path.join('/mnt/root', config.bootMountPoint, 'system-proxy')
redsocksConfPath = path.join(proxyBasePath, 'redsocks.conf')
noProxyPath = path.join(proxyBasePath, 'no_proxy')
readProxy = ->
fs.readFileAsync(redsocksConfPath)
.then (redsocksConf) ->
lines = new String(redsocksConf).split('\n')
conf = {}
for line in lines
for proxyField in proxyFields
if proxyField in [ 'login', 'password' ]
m = line.match(new RegExp(proxyField + '\\s*=\\s*\"(.*)\"\\s*;'))
else
m = line.match(new RegExp(proxyField + '\\s*=\\s*([^;\\s]*)\\s*;'))
if m?
conf[proxyField] = m[1]
return conf
.catch ENOENT, ->
return null
.then (conf) ->
if !conf?
return null
else
fs.readFileAsync(noProxyPath)
.then (noProxy) ->
conf.noProxy = new String(noProxy).split('\n')
return conf
.catch ENOENT, ->
return conf
generateRedsocksConfEntries = (conf) ->
val = ''
for field in proxyFields
if conf[field]?
v = conf[field]
if field in [ 'login', 'password' ]
v = "\"#{v}\""
val += "\t#{field} = #{v};\n"
return val
setProxy = (conf) ->
Promise.try ->
if _.isEmpty(conf)
fs.unlinkAsync(redsocksConfPath)
.catch(ENOENT, _.noop)
.then ->
fs.unlinkAsync(noProxyPath)
.catch(ENOENT, _.noop)
else
mkdirp(proxyBasePath)
.then ->
if _.isArray(conf.noProxy)
writeFileAtomic(noProxyPath, conf.noProxy.join('\n'))
.then ->
redsocksConf = ''
redsocksConf += redsocksHeader
redsocksConf += generateRedsocksConfEntries(conf)
redsocksConf += redsocksFooter
writeFileAtomic(redsocksConfPath, redsocksConf)
.then ->
utils.restartSystemdService('resin-proxy-config')
.then ->
utils.restartSystemdService('redsocks')
hostnamePath = '/mnt/root/etc/hostname'
readHostname = ->
fs.readFileAsync(hostnamePath)
.then (hostnameData) ->
return _.trim(new String(hostnameData))
setHostname = (val) ->
configJson.set(hostname: val)
.then ->
utils.restartSystemdService('resin-hostname')
exports.get = ->
Promise.join(
readProxy()
readHostname()
(proxy, hostname) ->
return {
network: {
proxy
hostname
}
}
)
exports.patch = (conf) ->
Promise.try ->
if !_.isUndefined(conf?.network?.proxy)
setProxy(conf.network.proxy)
.then ->
if !_.isUndefined(conf?.network?.hostname)
setHostname(conf.network.hostname)

16
src/lib/fs-utils.coffee Normal file
View File

@ -0,0 +1,16 @@
Promise = require 'bluebird'
fs = Promise.promisifyAll(require('fs'))
exports.writeAndSyncFile = (path, data) ->
fs.openAsync(path, 'w')
.then (fd) ->
fs.writeAsync(fd, data, 0, 'utf8')
.then ->
fs.fsyncAsync(fd)
.then ->
fs.closeAsync(fd)
exports.writeFileAtomic = (path, data) ->
exports.writeAndSyncFile("#{path}.new", data)
.then ->
fs.renameAsync("#{path}.new", path)

View File

@ -236,6 +236,14 @@ exports.vpnControl = (val, logMessage, { initial = false } = {}) ->
return false
.catchReturn(false)
exports.restartSystemdService = (serviceName) ->
gosuper.postAsync('/v1/restart-service', { json: true, body: Name: serviceName })
.spread (response, body) ->
if response.statusCode != 200
err = new Error("Error restarting service #{serviceName}: #{response.statusCode} #{body}")
err.statusCode = response.statusCode
throw err
exports.AppNotFoundError = class AppNotFoundError extends TypedError
exports.getKnexApp = (appId, columns) ->