Merge pull request #113 from resin-io/107-log-to-display-var

Host config improvements
This commit is contained in:
Pablo Carranza Vélez 2016-03-24 16:59:29 +02:00
commit 3adc752bc2
5 changed files with 122 additions and 22 deletions

View File

@ -1,3 +1,7 @@
* Add RESIN_HOST_LOG_TO_DISPLAY variable [Pablo]
* Add system logs for special actions and host config [Pablo]
* Fix setting config.txt for RPi 3 [Pablo]
* Fix saving config vars to DB before reboot [Pablo]
* Bind mount host /var/lib/connman to application /host_var/lib/connman [Aleksis]
* Add RESIN_SUPERVISOR_DELTA to special list so that app is not restarted when it changes [Pablo]

View File

@ -34,6 +34,10 @@ type VPNBody struct {
Enable bool
}
type LogToDisplayBody struct {
Enable bool
}
func jsonResponse(writer http.ResponseWriter, response interface{}, status int) {
jsonBody, err := json.Marshal(response)
if err != nil {
@ -67,12 +71,6 @@ func parsePurgeBody(request *http.Request) (appId string, err error) {
return
}
func responseSender(writer http.ResponseWriter) func(interface{}, string, int) {
return func(data interface{}, errorMsg string, statusCode int) {
jsonResponse(writer, APIResponse{data, errorMsg}, statusCode)
}
}
func responseSenders(writer http.ResponseWriter) (sendResponse func(interface{}, string, int), sendError func(error)) {
sendResponse = func(data interface{}, errorMsg string, statusCode int) {
jsonResponse(writer, APIResponse{data, errorMsg}, statusCode)
@ -221,3 +219,52 @@ func VPNControl(writer http.ResponseWriter, request *http.Request) {
log.Printf("%sd\n", actionDescr)
sendResponse("OK", "", http.StatusAccepted)
}
//LogToDisplayControl is used to control tty-replacement service status with dbus
func LogToDisplayControl(writer http.ResponseWriter, request *http.Request) {
sendResponse, sendError := responseSenders(writer)
serviceName := "tty-replacement.service"
var body LogToDisplayBody
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 set log to display state."))
return
}
if activeState, err := systemd.Dbus.GetUnitProperty(serviceName, "ActiveState"); err != nil {
sendError(fmt.Errorf("Unable to get log to display status: %v", err))
return
} else {
status := activeState.Value.String() == `"active"`
enable := body.Enable
if status == enable {
// Nothing to do, return Data = false to signal nothing was changed
sendResponse(false, "", http.StatusOK)
return
} else if enable {
if _, err := systemd.Dbus.StartUnit(serviceName, "fail", nil); err != nil {
sendError(fmt.Errorf("Unable to start service: %v", err))
return
}
if _, _, err = systemd.Dbus.EnableUnitFiles([]string{serviceName}, false, false); err != nil {
sendError(fmt.Errorf("Unable to enable service: %v", err))
return
}
} else {
if _, err := systemd.Dbus.StopUnit(serviceName, "fail", nil); err != nil {
sendError(fmt.Errorf("Unable to stop service: %v", err))
return
}
if _, err = systemd.Dbus.DisableUnitFiles([]string{serviceName}, false); err != nil {
sendError(fmt.Errorf("Unable to disable service: %v", err))
return
}
}
sendResponse(true, "", http.StatusOK)
}
}

View File

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

View File

@ -68,6 +68,10 @@ logTypes =
eventName: 'Application update error'
humanName: 'Failed to update application'
logSystemMessage = (message, obj, eventName) ->
logger.log({ message, isSystem: true })
utils.mixpanelTrack(eventName ? message, obj)
logSystemEvent = (logType, app, error) ->
message = "#{logType.humanName} '#{app.imageId}'"
if error?
@ -80,10 +84,16 @@ logSystemEvent = (logType, app, error) ->
if _.isEmpty(errMessage)
errMessage = 'Unknown cause'
message += " due to '#{errMessage}'"
logger.log({ message, isSystem: true })
utils.mixpanelTrack(logType.eventName, {app, error})
logSystemMessage(message, {app, error}, logType.eventName)
return
logSpecialAction = (action, value, success) ->
if success
msg = "Applied config variable #{action} = #{value}"
else
msg = "Applying config variable #{action} = #{value}"
logSystemMessage(msg, {}, "Apply special action #{success ? "success" : "in progress"}")
application = {}
application.kill = kill = (app, updateDB = true) ->
@ -334,18 +344,20 @@ specialActionEnvVars =
executedSpecialActionEnvVars = {}
executeSpecialActionsAndBootConfig = (env) ->
executeSpecialActionsAndHostConfig = (env) ->
Promise.try ->
_.map specialActionEnvVars, (specialActionCallback, key) ->
if env[key]? && specialActionCallback?
# This makes the Special Action Envs only trigger their functions once.
if !_.has(executedSpecialActionEnvVars, key) or executedSpecialActionEnvVars[key] != env[key]
logSpecialAction(key, env[key])
specialActionCallback(env[key])
executedSpecialActionEnvVars[key] = env[key]
bootConfigVars = _.pick env, (val, key) ->
return _.startsWith(key, device.bootConfigEnvVarPrefix)
if !_.isEmpty(bootConfigVars)
device.setBootConfig(bootConfigVars)
logSpecialAction(key, env[key], true)
hostConfigVars = _.pick env, (val, key) ->
return _.startsWith(key, device.hostConfigEnvVarPrefix)
if !_.isEmpty(hostConfigVars)
device.setHostConfig(hostConfigVars, logSystemMessage)
wrapAsError = (err) ->
return err if _.isError(err)
@ -460,6 +472,8 @@ getEnvAndFormatRemoteApps = (deviceId, remoteApps, uuid, apiKey) ->
utils.extendEnvVars(app.environment_variable, uuid)
.then (fullEnv) ->
env = _.omit(fullEnv, _.keys(specialActionEnvVars))
env = _.omit env, (v, k) ->
_.startsWith(k, device.hostConfigEnvVarPrefix)
return [
{
appId: '' + app.id
@ -482,7 +496,9 @@ formatLocalApps = (apps) ->
localAppEnvs = {}
localApps = _.mapValues apps, (app) ->
localAppEnvs[app.appId] = JSON.parse(app.env)
app.env = JSON.stringify(_.omit(localAppEnvs[app.appId], _.keys(specialActionEnvVars)))
app.env = _.omit localAppEnvs[app.appId], (v, k) ->
_.startsWith(k, device.hostConfigEnvVarPrefix)
app.env = JSON.stringify(_.omit(app.env, _.keys(specialActionEnvVars)))
app = _.pick(app, [ 'appId', 'commit', 'imageId', 'env' ])
return { localApps, localAppEnvs }
@ -527,8 +543,8 @@ application.update = update = (force) ->
# Run special functions against variables if remoteAppEnvs has the corresponding variable function mapping.
Promise.map appsWithChangedEnvs, (appId) ->
Promise.using lockUpdates(remoteApps[appId], force), ->
executeSpecialActionsAndBootConfig(remoteAppEnvs[appId])
.then ->
executeSpecialActionsAndHostConfig(remoteAppEnvs[appId])
.tap ->
# If an env var shouldn't cause a restart but requires an action, we should still
# save the new env to the DB
if !_.includes(toBeUpdated, appId) and !_.includes(toBeInstalled, appId)
@ -538,6 +554,8 @@ application.update = update = (force) ->
throw new Error('App not found')
app.env = JSON.stringify(remoteAppEnvs[appId])
knex('app').update(app).where({ appId })
.then (needsReboot) ->
device.reboot() if needsReboot
.catch (err) ->
logSystemEvent(logTypes.updateAppError, remoteApps[appId], err)
.return(allAppIds)
@ -613,7 +631,7 @@ application.initialize = ->
knex('app').select()
.then (apps) ->
Promise.map apps, (app) ->
executeSpecialActionsAndBootConfig(JSON.parse(app.env))
executeSpecialActionsAndHostConfig(JSON.parse(app.env))
.then ->
unlockAndStart(app)
.catch (error) ->

View File

@ -35,10 +35,11 @@ exports.getID = do ->
throw new Error('Could not find this device?!')
return devices[0].id
rebootDevice = ->
exports.reboot = rebootDevice = ->
request.postAsync(config.gosuperAddress + '/v1/reboot')
exports.bootConfigEnvVarPrefix = bootConfigEnvVarPrefix = 'RESIN_HOST_CONFIG_'
exports.hostConfigEnvVarPrefix = hostConfigEnvVarPrefix = 'RESIN_HOST_'
bootConfigEnvVarPrefix = 'RESIN_HOST_CONFIG_'
bootBlockDevice = '/dev/mmcblk0p1'
bootMountPoint = '/mnt/root/boot'
bootConfigPath = bootMountPoint + '/config.txt'
@ -69,10 +70,32 @@ parseBootConfigFromEnv = (env) ->
parsedEnv = _.omit(parsedEnv, forbiddenConfigKeys)
return parsedEnv
exports.setBootConfig = (env) ->
exports.setHostConfig = (env, logMessage) ->
Promise.join setBootConfig(env, logMessage), setLogToDisplay(env, logMessage), (bootConfigApplied, logToDisplayChanged) ->
return (bootConfigApplied or logToDisplayChanged)
setLogToDisplay = (env, logMessage) ->
if env['RESIN_HOST_LOG_TO_DISPLAY']?
enable = env['RESIN_HOST_LOG_TO_DISPLAY'] != '0'
request.postAsync(config.gosuperAddress + '/v1/set-log-to-display', {json: true, body: Enable: enable})
.spread (response, body) ->
if response.statusCode != 200
logMessage("Error setting log to display: #{body.Error}, Status:, #{response.statusCode}", {error: body.Error}, "Set log to display error")
return false
else
if body.Data == true
logMessage("#{if enable then "Enabled" else "Disabled"} logs to display")
return body.Data
.catch (err) ->
logMessage("Error setting log to display: #{err}", {error: err}, "Set log to display error")
return false
else
return Promise.resolve(false)
setBootConfig = (env, logMessage) ->
device.getDeviceType()
.then (deviceType) ->
throw new Error('This is not a Raspberry Pi') if !_.startsWith(deviceType, 'raspberry-pi')
throw new Error('This is not a Raspberry Pi') if !_.startsWith(deviceType, 'raspberry')
Promise.join parseBootConfigFromEnv(env), fs.readFileAsync(bootConfigPath, 'utf8'), (configFromApp, configTxt ) ->
throw new Error('No boot config to change') if _.isEmpty(configFromApp)
configFromFS = {}
@ -94,6 +117,8 @@ exports.setBootConfig = (env) ->
toBeChanged = _.filter toBeChanged, (key) ->
configFromApp[key] != configFromFS[key]
throw new Error('Nothing to change') if _.isEmpty(toBeChanged) and _.isEmpty(toBeAdded)
logMessage("Applying boot config: #{JSON.stringify(configFromApp)}", {}, "Apply boot config in progress")
# We add the keys to be added first so they are out of any filters
outputConfig = _.map toBeAdded, (key) -> "#{key}=#{configFromApp[key]}"
outputConfig = outputConfig.concat _.map configPositions, (key, index) ->
@ -112,9 +137,14 @@ exports.setBootConfig = (env) ->
.then ->
execAsync('sync')
.then ->
rebootDevice()
logMessage("Applied boot config: #{JSON.stringify(configFromApp)}", {}, "Apply boot config success")
return true
.catch (err) ->
logMessage("Error setting boot config: #{err}", {error: err}, "Apply boot config error")
throw err
.catch (err) ->
console.log('Will not set boot config: ', err)
return false
exports.getDeviceType = do ->
deviceTypePromise = null