blob: 05ffcf9d7b059450b8b5539c25368631294765fe [file] [log] [blame]
/*
* Copyright 2020-2024 Open Networking Foundation (ONF) and the ONF Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Package swupg provides the utilities for onu sw upgrade
package swupg
import (
"bufio"
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"sync"
"time"
"github.com/opencord/voltha-lib-go/v7/pkg/log"
)
const cDefaultLocalDir = "/tmp" //this is the default local dir to download to
// FileState defines the download state of the ONU software image
type FileState uint32
//nolint:varcheck, deadcode
const (
//CFileStateUnknown: the download state is not really known
CFileStateUnknown FileState = iota
//CFileStateDlStarted: the download to adapter has been started
CFileStateDlStarted
//CFileStateDlSucceeded: the download to adapter is successfully done (file exists and ready to use)
CFileStateDlSucceeded
//CFileStateDlFailed: the download to adapter has failed
CFileStateDlFailed
//CFileStateDlAborted: the download to adapter was aborted
CFileStateDlAborted
)
type downloadImageParams struct {
downloadImageName string
downloadImageURL string
downloadImageState FileState
downloadImageLen int64
downloadImageCRC uint32
downloadActive bool
downloadContextCancelFn context.CancelFunc
}
type requesterChannelMap map[chan<- bool]struct{} //using an empty structure map for easier (unique) element appending
// FileDownloadManager structure holds information needed for downloading to and storing images within the adapter
type FileDownloadManager struct {
mutexFileState sync.RWMutex
mutexDownloadImageDsc sync.RWMutex
downloadImageDscSlice []downloadImageParams
dnldImgReadyWaiting map[string]requesterChannelMap
dlToAdapterTimeout time.Duration
}
// NewFileDownloadManager constructor returns a new instance of a FileDownloadManager
// mib_db (as well as not inluded alarm_db not really used in this code? VERIFY!!)
func NewFileDownloadManager(ctx context.Context) *FileDownloadManager {
logger.Debug(ctx, "init-FileDownloadManager")
var localDnldMgr FileDownloadManager
localDnldMgr.downloadImageDscSlice = make([]downloadImageParams, 0)
localDnldMgr.dnldImgReadyWaiting = make(map[string]requesterChannelMap)
localDnldMgr.dlToAdapterTimeout = 10 * time.Second //default timeout, should be overwritten immediately after start
return &localDnldMgr
}
// SetDownloadTimeout configures the timeout used to supervice the download of the image to the adapter (assumed in seconds)
func (dm *FileDownloadManager) SetDownloadTimeout(ctx context.Context, aDlTimeout time.Duration) {
dm.mutexDownloadImageDsc.Lock()
defer dm.mutexDownloadImageDsc.Unlock()
logger.Debugw(ctx, "setting download timeout", log.Fields{"timeout": aDlTimeout})
dm.dlToAdapterTimeout = aDlTimeout
}
// GetDownloadTimeout delivers the timeout used to supervice the download of the image to the adapter (assumed in seconds)
func (dm *FileDownloadManager) GetDownloadTimeout(ctx context.Context) time.Duration {
dm.mutexDownloadImageDsc.RLock()
defer dm.mutexDownloadImageDsc.RUnlock()
return dm.dlToAdapterTimeout
}
// StartDownload returns FileState and error code from download request for the given file name and URL
func (dm *FileDownloadManager) StartDownload(ctx context.Context, aImageName string, aURLCommand string) (FileState, error) {
logger.Infow(ctx, "image download-to-adapter requested", log.Fields{
"image-name": aImageName, "url-command": aURLCommand})
// keep a semaphore over the complete method in order to avoid parallel entrance to this method
// otherwise a temporary file state 'Started' could be indicated allowing start of ONU upgrade handling
// even though the download-start to adapter may fail (e.g on wrong URL) (delivering inconsistent download results)
// so once called the download-start of the first call must have been completely checked before another execution
// could still be limited to the same imageName, but that should be no real gain
dm.mutexFileState.Lock()
defer dm.mutexFileState.Unlock()
dm.mutexDownloadImageDsc.Lock()
var fileState FileState
var exists bool
if fileState, exists = dm.imageExists(ctx, aImageName, aURLCommand); !exists {
loDownloadImageParams := downloadImageParams{
downloadImageName: aImageName, downloadImageURL: aURLCommand, downloadImageState: CFileStateDlStarted,
downloadImageLen: 0, downloadImageCRC: 0}
dm.downloadImageDscSlice = append(dm.downloadImageDscSlice, loDownloadImageParams)
dm.mutexDownloadImageDsc.Unlock()
//start downloading from server
var err error
if err = dm.downloadFile(ctx, aURLCommand, cDefaultLocalDir, aImageName); err == nil {
//indicate download started correctly, complete download may run in background
return CFileStateDlStarted, nil
}
//return the error result of the download-request
return CFileStateUnknown, err
}
dm.mutexDownloadImageDsc.Unlock()
if fileState == CFileStateUnknown {
//cannot simply remove the existing file here - might still be used for running upgrades on different devices!
// (has to be removed by operator - cancel API)
logger.Errorw(ctx, "image download requested for existing file with different URL",
log.Fields{"image-description": aImageName, "url": aURLCommand})
return fileState, fmt.Errorf("existing file is based on different URL, requested URL: %s", aURLCommand)
}
logger.Debugw(ctx, "image download already started or done", log.Fields{"image-description": aImageName})
return fileState, nil
}
// GetImageBufferLen returns the length of the specified file in bytes (file size) - as detected after download
func (dm *FileDownloadManager) GetImageBufferLen(ctx context.Context, aFileName string) (int64, error) {
dm.mutexDownloadImageDsc.RLock()
defer dm.mutexDownloadImageDsc.RUnlock()
for _, dnldImgDsc := range dm.downloadImageDscSlice {
if dnldImgDsc.downloadImageName == aFileName && dnldImgDsc.downloadImageState == CFileStateDlSucceeded {
//image found (by name) and fully downloaded
return dnldImgDsc.downloadImageLen, nil
}
}
return 0, fmt.Errorf("no downloaded image found: %s", aFileName)
}
// GetDownloadImageBuffer returns the content of the requested file as byte slice
// precondition: it is assumed that a check is done immediately before if the file was downloaded to adapter correctly from caller
//
// straightforward approach is here to e.g. immediately call and verify GetImageBufferLen() before this
func (dm *FileDownloadManager) GetDownloadImageBuffer(ctx context.Context, aFileName string) ([]byte, error) {
file, err := os.Open(filepath.Clean(cDefaultLocalDir + "/" + aFileName))
if err != nil {
return nil, err
}
defer func() {
err := file.Close()
if err != nil {
logger.Errorw(ctx, "failed to close file", log.Fields{"error": err})
}
}()
stats, statsErr := file.Stat()
if statsErr != nil {
return nil, statsErr
}
var size int64 = stats.Size()
bytes := make([]byte, size)
buffer := bufio.NewReader(file)
_, err = buffer.Read(bytes)
return bytes, err
}
// RequestDownloadReady receives a channel that has to be used to inform the requester in case the concerned file is downloaded
func (dm *FileDownloadManager) RequestDownloadReady(ctx context.Context, aFileName string, aWaitChannel chan<- bool) {
//mutexDownloadImageDsc must already be locked here to avoid an update of the dnldImgReadyWaiting map
// just after returning false on imageLocallyDownloaded() (not found) and immediate handling of the
// download success (within updateFileState())
// so updateFileState() can't interfere here just after imageLocallyDownloaded() before setting the requester map
dm.mutexDownloadImageDsc.Lock()
defer dm.mutexDownloadImageDsc.Unlock()
if dm.imageLocallyDownloaded(ctx, aFileName) {
//image found (by name) and fully downloaded
logger.Debugw(ctx, "file ready - immediate response", log.Fields{"image-name": aFileName})
aWaitChannel <- true
return
}
//when we are here the image was not yet found or not fully downloaded -
// add the device specific channel to the list of waiting requesters
if loRequesterChannelMap, ok := dm.dnldImgReadyWaiting[aFileName]; ok {
//entry for the file name already exists
if _, exists := loRequesterChannelMap[aWaitChannel]; !exists {
// requester channel does not yet exist for the image
loRequesterChannelMap[aWaitChannel] = struct{}{}
dm.dnldImgReadyWaiting[aFileName] = loRequesterChannelMap
logger.Debugw(ctx, "file not ready - adding new requester", log.Fields{
"image-name": aFileName, "number-of-requesters": len(dm.dnldImgReadyWaiting[aFileName])})
}
} else {
//entry for the file name does not even exist
addRequesterChannelMap := make(map[chan<- bool]struct{})
addRequesterChannelMap[aWaitChannel] = struct{}{}
dm.dnldImgReadyWaiting[aFileName] = addRequesterChannelMap
logger.Debugw(ctx, "file not ready - setting first requester", log.Fields{
"image-name": aFileName})
}
}
// RemoveReadyRequest removes the specified channel from the requester(channel) map for the given file name
func (dm *FileDownloadManager) RemoveReadyRequest(ctx context.Context, aFileName string, aWaitChannel chan bool) {
dm.mutexDownloadImageDsc.Lock()
defer dm.mutexDownloadImageDsc.Unlock()
for imageName, channelMap := range dm.dnldImgReadyWaiting {
if imageName == aFileName {
for channel := range channelMap {
if channel == aWaitChannel {
delete(dm.dnldImgReadyWaiting[imageName], channel)
logger.Debugw(ctx, "channel removed from the requester map", log.Fields{
"image-name": aFileName, "new number-of-requesters": len(dm.dnldImgReadyWaiting[aFileName])})
return //can leave directly
}
}
return //can leave directly
}
}
}
// FileDownloadManager private (unexported) methods -- start
// imageExists returns current ImageState and if the requested image already exists within the adapter
// precondition: at calling this method mutexDownloadImageDsc must already be at least RLocked by the caller
func (dm *FileDownloadManager) imageExists(ctx context.Context, aImageName string, aURL string) (FileState, bool) {
logger.Debugw(ctx, "checking on existence of the image", log.Fields{"image-name": aImageName})
for _, dnldImgDsc := range dm.downloadImageDscSlice {
if dnldImgDsc.downloadImageName == aImageName {
//image found (by name)
if dnldImgDsc.downloadImageURL == aURL {
//image found (by name and URL)
return dnldImgDsc.downloadImageState, true
}
return CFileStateUnknown, true //use fileState to indicate URL mismatch for existing file
}
}
//image not found (by name)
return CFileStateUnknown, false
}
// imageLocallyDownloaded returns true if the requested image already exists within the adapter
//
// requires mutexDownloadImageDsc to be locked (at least RLocked)
func (dm *FileDownloadManager) imageLocallyDownloaded(ctx context.Context, aImageName string) bool {
logger.Debugw(ctx, "checking if image is fully downloaded to adapter", log.Fields{"image-name": aImageName})
for _, dnldImgDsc := range dm.downloadImageDscSlice {
if dnldImgDsc.downloadImageName == aImageName {
//image found (by name)
if dnldImgDsc.downloadImageState == CFileStateDlSucceeded {
logger.Debugw(ctx, "image has been fully downloaded", log.Fields{"image-name": aImageName})
return true
}
logger.Debugw(ctx, "image not yet fully downloaded", log.Fields{"image-name": aImageName})
return false
}
}
//image not found (by name)
logger.Errorw(ctx, "image does not exist", log.Fields{"image-name": aImageName})
return false
}
// updateDownloadCancel sets context cancel function to be used in case the download is to be aborted
func (dm *FileDownloadManager) updateDownloadCancel(ctx context.Context,
aImageName string, aCancelFn context.CancelFunc) {
dm.mutexDownloadImageDsc.Lock()
defer dm.mutexDownloadImageDsc.Unlock()
for imgKey, dnldImgDsc := range dm.downloadImageDscSlice {
if dnldImgDsc.downloadImageName == aImageName {
//image found (by name) - need to write changes on the original map
dm.downloadImageDscSlice[imgKey].downloadContextCancelFn = aCancelFn
dm.downloadImageDscSlice[imgKey].downloadActive = true
logger.Debugw(ctx, "downloadContextCancelFn set", log.Fields{
"image-name": aImageName})
return //can leave directly
}
}
}
// updateFileState sets the new active (downloaded) file state and informs possibly waiting requesters on this change
func (dm *FileDownloadManager) updateFileState(ctx context.Context, aImageName string, aFileSize int64) {
dm.mutexDownloadImageDsc.Lock()
defer dm.mutexDownloadImageDsc.Unlock()
for imgKey, dnldImgDsc := range dm.downloadImageDscSlice {
if dnldImgDsc.downloadImageName == aImageName {
//image found (by name) - need to write changes on the original map
dm.downloadImageDscSlice[imgKey].downloadActive = false
dm.downloadImageDscSlice[imgKey].downloadImageState = CFileStateDlSucceeded
dm.downloadImageDscSlice[imgKey].downloadImageLen = aFileSize
logger.Debugw(ctx, "imageState download succeeded", log.Fields{
"image-name": aImageName, "image-size": aFileSize})
//in case upgrade process(es) was/were waiting for the file, inform them
for imageName, channelMap := range dm.dnldImgReadyWaiting {
if imageName == aImageName {
for channel := range channelMap {
// use all found channels to inform possible requesters about the existence of the file
channel <- true
delete(dm.dnldImgReadyWaiting[imageName], channel) //requester served
}
return //can leave directly
}
}
return //can leave directly
}
}
}
// downloadFile downloads the specified file from the given http location
func (dm *FileDownloadManager) downloadFile(ctx context.Context, aURLCommand string, aFilePath string, aFileName string) error {
// Get the data
logger.Infow(ctx, "downloading with URL", log.Fields{"url": aURLCommand, "localPath": aFilePath})
// verifying the complete URL by parsing it to its URL elements
urlBase, err1 := url.Parse(aURLCommand)
if err1 != nil {
logger.Errorw(ctx, "could not set base url command", log.Fields{"url": aURLCommand, "error": err1})
dm.removeImage(ctx, aFileName, false) //wo FileSystem access
return fmt.Errorf("could not set base url command: %s, error: %s", aURLCommand, err1)
}
urlParams := url.Values{}
urlBase.RawQuery = urlParams.Encode()
//pre-check on file existence - assuming http location here
reqExist, errExist2 := http.NewRequest("HEAD", urlBase.String(), nil)
if errExist2 != nil {
logger.Errorw(ctx, "could not generate http head request", log.Fields{"url": urlBase.String(), "error": errExist2})
dm.removeImage(ctx, aFileName, false) //wo FileSystem access
return fmt.Errorf("could not generate http head request: %s, error: %s", aURLCommand, errExist2)
}
ctxExist, cancelExist := context.WithDeadline(ctx, time.Now().Add(3*time.Second)) //waiting for some fast answer
defer cancelExist()
_ = reqExist.WithContext(ctxExist)
respExist, errExist3 := http.DefaultClient.Do(reqExist)
if errExist3 != nil || respExist.StatusCode != http.StatusOK {
if respExist == nil {
logger.Errorw(ctx, "http head from url error - no status, aborting", log.Fields{"url": urlBase.String(),
"error": errExist3})
dm.removeImage(ctx, aFileName, false) //wo FileSystem access
return fmt.Errorf("http head from url error - no status, aborting: %s, error: %s",
aURLCommand, errExist3)
}
logger.Infow(ctx, "could not http head from url", log.Fields{"url": urlBase.String(),
"error": errExist3, "status": respExist.StatusCode})
//if head is not supported by server we cannot use this test and just try to continue
if respExist.StatusCode != http.StatusMethodNotAllowed {
logger.Errorw(ctx, "http head from url: file does not exist here, aborting", log.Fields{"url": urlBase.String(),
"error": errExist3, "status": respExist.StatusCode})
dm.removeImage(ctx, aFileName, false) //wo FileSystem access
return fmt.Errorf("http head from url: file does not exist here, aborting: %s, error: %s, status: %d",
aURLCommand, errExist3, respExist.StatusCode)
}
}
defer func() {
deferredErr := respExist.Body.Close()
if deferredErr != nil {
logger.Errorw(ctx, "error at closing http head response body", log.Fields{"url": urlBase.String(), "error": deferredErr})
}
}()
//trying to download - do it in background as it may take some time ...
go func() {
req, err2 := http.NewRequest("GET", urlBase.String(), nil)
if err2 != nil {
logger.Errorw(ctx, "could not generate http request", log.Fields{"url": urlBase.String(), "error": err2})
dm.removeImage(ctx, aFileName, false) //wo FileSystem access
return
}
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(dm.dlToAdapterTimeout)) //timeout as given from SetDownloadTimeout()
dm.updateDownloadCancel(ctx, aFileName, cancel)
defer cancel()
_ = req.WithContext(ctx)
resp, err3 := http.DefaultClient.Do(req)
if err3 != nil || resp.StatusCode != http.StatusOK {
if resp == nil {
logger.Errorw(ctx, "http get error - no status, aborting", log.Fields{"url": urlBase.String(),
"error": err3})
} else {
logger.Errorw(ctx, "could not http get from url", log.Fields{"url": urlBase.String(),
"error": err3, "status": resp.StatusCode})
}
dm.removeImage(ctx, aFileName, false) //wo FileSystem access
return
}
defer func() {
deferredErr := resp.Body.Close()
if deferredErr != nil {
logger.Errorw(ctx, "error at closing http get response body", log.Fields{"url": urlBase.String(), "error": deferredErr})
}
}()
// Create the file
aLocalPathName := aFilePath + "/" + aFileName
file, err := os.Create(aLocalPathName)
if err != nil {
logger.Errorw(ctx, "could not create local file", log.Fields{"path_file": aLocalPathName, "error": err})
dm.removeImage(ctx, aFileName, false) //wo FileSystem access
return
}
defer func() {
deferredErr := file.Close()
if deferredErr != nil {
logger.Errorw(ctx, "error at closing new file", log.Fields{"path_file": aLocalPathName, "error": deferredErr})
}
}()
// Write the body to file
_, err = io.Copy(file, resp.Body)
if err != nil {
logger.Errorw(ctx, "could not copy file content", log.Fields{"url": urlBase.String(), "file": aLocalPathName, "error": err})
dm.removeImage(ctx, aFileName, true)
return
}
fileStats, statsErr := file.Stat()
if err != nil {
logger.Errorw(ctx, "created file can't be accessed", log.Fields{"file": aLocalPathName, "stat-error": statsErr})
return
}
fileSize := fileStats.Size()
logger.Infow(ctx, "written file size is", log.Fields{"file": aLocalPathName, "length": fileSize})
dm.updateFileState(ctx, aFileName, fileSize)
//TODO:!!! further extension could be provided here, e.g. already computing and possibly comparing the CRC, vendor check
}()
return nil
}
// removeImage deletes the given image according to the Image name from filesystem and downloadImageDscSlice
func (dm *FileDownloadManager) removeImage(ctx context.Context, aImageName string, aDelFs bool) {
logger.Debugw(ctx, "remove the image from Adapter", log.Fields{"image-name": aImageName, "del-fs": aDelFs})
dm.mutexDownloadImageDsc.RLock()
defer dm.mutexDownloadImageDsc.RUnlock()
tmpSlice := dm.downloadImageDscSlice[:0]
for _, dnldImgDsc := range dm.downloadImageDscSlice {
if dnldImgDsc.downloadImageName == aImageName {
//image found (by name)
logger.Debugw(ctx, "removing image", log.Fields{"image-name": aImageName})
if aDelFs {
//remove the image from filesystem
aLocalPathName := cDefaultLocalDir + "/" + aImageName
if err := os.Remove(aLocalPathName); err != nil {
// might be a temporary situation, when the file was not yet (completely) written
logger.Debugw(ctx, "image not removed from filesystem", log.Fields{
"image-name": aImageName, "error": err})
}
}
// and remove from the imageDsc slice by just not appending
} else {
tmpSlice = append(tmpSlice, dnldImgDsc)
}
}
dm.downloadImageDscSlice = tmpSlice
//image not found (by name)
}
// CancelDownload stops the download and clears all entires concerning this aimageName
func (dm *FileDownloadManager) CancelDownload(ctx context.Context, aImageName string) {
// for the moment that would only support to wait for the download end and remove the image then
// further reactions while still downloading can be considered with some effort, but does it make sense (synchronous load here!)
dm.mutexDownloadImageDsc.RLock()
for imgKey, dnldImgDsc := range dm.downloadImageDscSlice {
if dnldImgDsc.downloadImageName == aImageName {
//image found (by name) - need to to check on ongoing download
if dm.downloadImageDscSlice[imgKey].downloadActive {
//then cancel the download using the context cancel function
dm.downloadImageDscSlice[imgKey].downloadContextCancelFn()
}
//and remove possibly stored traces of this image
dm.mutexDownloadImageDsc.RUnlock()
go dm.removeImage(ctx, aImageName, true) //including the chance that nothing was yet written to FS, should not matter
return //can leave directly
}
}
dm.mutexDownloadImageDsc.RUnlock()
}