| /* |
| * 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() |
| } |