blob: 00440c13e4f07e662dfd1554c94a85064b68f9dd [file] [log] [blame]
mpagenkoc26d4c02021-05-06 14:27:57 +00001/*
2 * Copyright 2020-present Open Networking Foundation
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//Package adaptercoreonu provides the utility for onu devices, flows and statistics
18package adaptercoreonu
19
20import (
21 "bufio"
22 "context"
23 "fmt"
24 "io"
25 "net/http"
26 "net/url"
27 "os"
Andrey Pozolotin1394a1c2021-06-01 00:54:18 +030028 "path/filepath"
mpagenkoc26d4c02021-05-06 14:27:57 +000029 "sync"
30 "time"
31
Girish Gowdra50e56422021-06-01 16:46:04 -070032 "github.com/opencord/voltha-lib-go/v5/pkg/log"
mpagenkoc26d4c02021-05-06 14:27:57 +000033)
34
35const cDefaultLocalDir = "/tmp" //this is the default local dir to download to
36
37type fileState uint32
38
39//nolint:varcheck, deadcode
40const (
41 cFileStateUnknown fileState = iota
42 cFileStateDlStarted
43 cFileStateDlSucceeded
44 cFileStateDlFailed
45 cFileStateDlAborted
46 cFileStateDlInvalid
47)
48
49type downloadImageParams struct {
50 downloadImageName string
51 downloadImageState fileState
52 downloadImageLen int64
53 downloadImageCrc uint32
54}
55
56type requesterChannelMap map[chan<- bool]struct{} //using an empty structure map for easier (unique) element appending
57
58//fileDownloadManager structure holds information needed for downloading to and storing images within the adapter
59type fileDownloadManager struct {
60 mutexDownloadImageDsc sync.RWMutex
61 downloadImageDscSlice []downloadImageParams
62 dnldImgReadyWaiting map[string]requesterChannelMap
63 dlToAdapterTimeout time.Duration
64}
65
66//newFileDownloadManager constructor returns a new instance of a fileDownloadManager
67//mib_db (as well as not inluded alarm_db not really used in this code? VERIFY!!)
68func newFileDownloadManager(ctx context.Context) *fileDownloadManager {
69 logger.Debug(ctx, "init-fileDownloadManager")
70 var localDnldMgr fileDownloadManager
71 localDnldMgr.downloadImageDscSlice = make([]downloadImageParams, 0)
72 localDnldMgr.dnldImgReadyWaiting = make(map[string]requesterChannelMap)
73 localDnldMgr.dlToAdapterTimeout = 10 * time.Second //default timeout, should be overwritten immediately after start
74 return &localDnldMgr
75}
76
77//SetDownloadTimeout configures the timeout used to supervice the download of the image to the adapter (assumed in seconds)
78func (dm *fileDownloadManager) SetDownloadTimeout(ctx context.Context, aDlTimeout time.Duration) {
79 dm.mutexDownloadImageDsc.Lock()
80 defer dm.mutexDownloadImageDsc.Unlock()
81 logger.Debugw(ctx, "setting download timeout", log.Fields{"timeout": aDlTimeout})
82 dm.dlToAdapterTimeout = aDlTimeout
83}
84
85//GetDownloadTimeout delivers the timeout used to supervice the download of the image to the adapter (assumed in seconds)
86func (dm *fileDownloadManager) GetDownloadTimeout(ctx context.Context) time.Duration {
87 dm.mutexDownloadImageDsc.RLock()
88 defer dm.mutexDownloadImageDsc.RUnlock()
89 return dm.dlToAdapterTimeout
90}
91
92//ImageExists returns true if the requested image already exists within the adapter
93func (dm *fileDownloadManager) ImageExists(ctx context.Context, aImageName string) bool {
94 logger.Debugw(ctx, "checking on existence of the image", log.Fields{"image-name": aImageName})
95 dm.mutexDownloadImageDsc.RLock()
96 defer dm.mutexDownloadImageDsc.RUnlock()
97
98 for _, dnldImgDsc := range dm.downloadImageDscSlice {
99 if dnldImgDsc.downloadImageName == aImageName {
100 //image found (by name)
101 return true
102 }
103 }
104 //image not found (by name)
105 return false
106}
107
108//StartDownload returns true if the download of the requested image could be started for the given file name and URL
109func (dm *fileDownloadManager) StartDownload(ctx context.Context, aImageName string, aURLCommand string) error {
110 logger.Infow(ctx, "image download-to-adapter requested", log.Fields{
111 "image-name": aImageName, "url-command": aURLCommand})
112 loDownloadImageParams := downloadImageParams{
113 downloadImageName: aImageName, downloadImageState: cFileStateDlStarted,
114 downloadImageLen: 0, downloadImageCrc: 0}
mpagenkoc26d4c02021-05-06 14:27:57 +0000115 //try to download from http
mpagenko38662d02021-08-11 09:45:19 +0000116 var err error
117 if err = dm.downloadFile(ctx, aURLCommand, cDefaultLocalDir, aImageName); err == nil {
118 dm.mutexDownloadImageDsc.Lock()
119 dm.downloadImageDscSlice = append(dm.downloadImageDscSlice, loDownloadImageParams)
120 dm.mutexDownloadImageDsc.Unlock()
121 }
mpagenkoc26d4c02021-05-06 14:27:57 +0000122 //return the result of the start-request to comfort the core processing even though the complete download may go on in background
123 return err
124}
125
126//GetImageBufferLen returns the length of the specified file in bytes (file size) - as detected after download
127func (dm *fileDownloadManager) GetImageBufferLen(ctx context.Context, aFileName string) (int64, error) {
128 dm.mutexDownloadImageDsc.RLock()
129 defer dm.mutexDownloadImageDsc.RUnlock()
130 for _, dnldImgDsc := range dm.downloadImageDscSlice {
131 if dnldImgDsc.downloadImageName == aFileName && dnldImgDsc.downloadImageState == cFileStateDlSucceeded {
132 //image found (by name) and fully downloaded
133 return dnldImgDsc.downloadImageLen, nil
134 }
135 }
136 return 0, fmt.Errorf("no downloaded image found: %s", aFileName)
137}
138
139//GetDownloadImageBuffer returns the content of the requested file as byte slice
140func (dm *fileDownloadManager) GetDownloadImageBuffer(ctx context.Context, aFileName string) ([]byte, error) {
Andrey Pozolotin1394a1c2021-06-01 00:54:18 +0300141 file, err := os.Open(filepath.Clean(cDefaultLocalDir + "/" + aFileName))
mpagenkoc26d4c02021-05-06 14:27:57 +0000142 if err != nil {
143 return nil, err
144 }
Andrey Pozolotin1394a1c2021-06-01 00:54:18 +0300145 defer func() {
146 err := file.Close()
147 if err != nil {
148 logger.Errorw(ctx, "failed to close file", log.Fields{"error": err})
149 }
150 }()
mpagenkoc26d4c02021-05-06 14:27:57 +0000151
152 stats, statsErr := file.Stat()
153 if statsErr != nil {
154 return nil, statsErr
155 }
156
157 var size int64 = stats.Size()
158 bytes := make([]byte, size)
159
160 buffer := bufio.NewReader(file)
161 _, err = buffer.Read(bytes)
162
163 return bytes, err
164}
165
166//RequestDownloadReady receives a channel that has to be used to inform the requester in case the concerned file is downloaded
167func (dm *fileDownloadManager) RequestDownloadReady(ctx context.Context, aFileName string, aWaitChannel chan<- bool) {
mpagenko5b5cb982021-08-18 16:38:51 +0000168 //mutexDownloadImageDsc must already be locked here to avoid an update of the dnldImgReadyWaiting map
169 // just after returning false on imageLocallyDownloaded() (not found) and immediate handling of the
170 // download success (within updateFileState())
171 // so updateFileState() can't interfere here just after imageLocallyDownloaded() before setting the requester map
172 dm.mutexDownloadImageDsc.Lock()
173 defer dm.mutexDownloadImageDsc.Unlock()
mpagenkoc26d4c02021-05-06 14:27:57 +0000174 if dm.imageLocallyDownloaded(ctx, aFileName) {
175 //image found (by name) and fully downloaded
176 logger.Debugw(ctx, "file ready - immediate response", log.Fields{"image-name": aFileName})
177 aWaitChannel <- true
178 return
179 }
180 //when we are here the image was not yet found or not fully downloaded -
181 // add the device specific channel to the list of waiting requesters
mpagenkoc26d4c02021-05-06 14:27:57 +0000182 if loRequesterChannelMap, ok := dm.dnldImgReadyWaiting[aFileName]; ok {
183 //entry for the file name already exists
184 if _, exists := loRequesterChannelMap[aWaitChannel]; !exists {
185 // requester channel does not yet exist for the image
186 loRequesterChannelMap[aWaitChannel] = struct{}{}
187 dm.dnldImgReadyWaiting[aFileName] = loRequesterChannelMap
188 logger.Debugw(ctx, "file not ready - adding new requester", log.Fields{
189 "image-name": aFileName, "number-of-requesters": len(dm.dnldImgReadyWaiting[aFileName])})
190 }
191 } else {
192 //entry for the file name does not even exist
193 addRequesterChannelMap := make(map[chan<- bool]struct{})
194 addRequesterChannelMap[aWaitChannel] = struct{}{}
195 dm.dnldImgReadyWaiting[aFileName] = addRequesterChannelMap
196 logger.Debugw(ctx, "file not ready - setting first requester", log.Fields{
197 "image-name": aFileName})
198 }
199}
200
201//RemoveReadyRequest removes the specified channel from the requester(channel) map for the given file name
202func (dm *fileDownloadManager) RemoveReadyRequest(ctx context.Context, aFileName string, aWaitChannel chan bool) {
203 dm.mutexDownloadImageDsc.Lock()
204 defer dm.mutexDownloadImageDsc.Unlock()
205 for imageName, channelMap := range dm.dnldImgReadyWaiting {
206 if imageName == aFileName {
207 for channel := range channelMap {
208 if channel == aWaitChannel {
209 delete(dm.dnldImgReadyWaiting[imageName], channel)
210 logger.Debugw(ctx, "channel removed from the requester map", log.Fields{
211 "image-name": aFileName, "new number-of-requesters": len(dm.dnldImgReadyWaiting[aFileName])})
212 return //can leave directly
213 }
214 }
215 return //can leave directly
216 }
217 }
218}
219
220// FileDownloadManager private (unexported) methods -- start
221
222//imageLocallyDownloaded returns true if the requested image already exists within the adapter
mpagenko5b5cb982021-08-18 16:38:51 +0000223// requires mutexDownloadImageDsc to be locked (at least RLocked)
mpagenkoc26d4c02021-05-06 14:27:57 +0000224func (dm *fileDownloadManager) imageLocallyDownloaded(ctx context.Context, aImageName string) bool {
225 logger.Debugw(ctx, "checking if image is fully downloaded to adapter", log.Fields{"image-name": aImageName})
mpagenkoc26d4c02021-05-06 14:27:57 +0000226 for _, dnldImgDsc := range dm.downloadImageDscSlice {
227 if dnldImgDsc.downloadImageName == aImageName {
228 //image found (by name)
229 if dnldImgDsc.downloadImageState == cFileStateDlSucceeded {
230 logger.Debugw(ctx, "image has been fully downloaded", log.Fields{"image-name": aImageName})
231 return true
232 }
233 logger.Debugw(ctx, "image not yet fully downloaded", log.Fields{"image-name": aImageName})
234 return false
235 }
236 }
237 //image not found (by name)
238 logger.Errorw(ctx, "image does not exist", log.Fields{"image-name": aImageName})
239 return false
240}
241
mpagenko5b5cb982021-08-18 16:38:51 +0000242//updateFileState sets the new active (downloaded) file state and informs possibly waiting requesters on this change
243func (dm *fileDownloadManager) updateFileState(ctx context.Context, aImageName string, aFileSize int64) {
244 dm.mutexDownloadImageDsc.Lock()
245 defer dm.mutexDownloadImageDsc.Unlock()
246 for imgKey, dnldImgDsc := range dm.downloadImageDscSlice {
247 if dnldImgDsc.downloadImageName == aImageName {
248 //image found (by name) - need to write changes on the original map
249 dm.downloadImageDscSlice[imgKey].downloadImageState = cFileStateDlSucceeded
250 dm.downloadImageDscSlice[imgKey].downloadImageLen = aFileSize
251 logger.Debugw(ctx, "imageState download succeeded", log.Fields{
252 "image-name": aImageName, "image-size": aFileSize})
253 //in case upgrade process(es) was/were waiting for the file, inform them
254 for imageName, channelMap := range dm.dnldImgReadyWaiting {
255 if imageName == aImageName {
256 for channel := range channelMap {
257 // use all found channels to inform possible requesters about the existence of the file
258 channel <- true
259 delete(dm.dnldImgReadyWaiting[imageName], channel) //requester served
260 }
261 return //can leave directly
262 }
263 }
264 return //can leave directly
265 }
266 }
267}
268
mpagenkoc26d4c02021-05-06 14:27:57 +0000269//downloadFile downloads the specified file from the given http location
270func (dm *fileDownloadManager) downloadFile(ctx context.Context, aURLCommand string, aFilePath string, aFileName string) error {
271 // Get the data
272 logger.Infow(ctx, "downloading with URL", log.Fields{"url": aURLCommand, "localPath": aFilePath})
273 // verifying the complete URL by parsing it to its URL elements
274 urlBase, err1 := url.Parse(aURLCommand)
275 if err1 != nil {
276 logger.Errorw(ctx, "could not set base url command", log.Fields{"url": aURLCommand, "error": err1})
277 return fmt.Errorf("could not set base url command: %s, error: %s", aURLCommand, err1)
278 }
279 urlParams := url.Values{}
280 urlBase.RawQuery = urlParams.Encode()
281
282 //pre-check on file existence - assuming http location here
283 reqExist, errExist2 := http.NewRequest("HEAD", urlBase.String(), nil)
284 if errExist2 != nil {
285 logger.Errorw(ctx, "could not generate http head request", log.Fields{"url": urlBase.String(), "error": errExist2})
286 return fmt.Errorf("could not generate http head request: %s, error: %s", aURLCommand, errExist2)
287 }
288 ctxExist, cancelExist := context.WithDeadline(ctx, time.Now().Add(3*time.Second)) //waiting for some fast answer
289 defer cancelExist()
290 _ = reqExist.WithContext(ctxExist)
291 respExist, errExist3 := http.DefaultClient.Do(reqExist)
292 if errExist3 != nil || respExist.StatusCode != http.StatusOK {
293 logger.Infow(ctx, "could not http head from url", log.Fields{"url": urlBase.String(),
294 "error": errExist3, "status": respExist.StatusCode})
295 //if head is not supported by server we cannot use this test and just try to continue
296 if respExist.StatusCode != http.StatusMethodNotAllowed {
297 logger.Errorw(ctx, "http head from url: file does not exist here, aborting", log.Fields{"url": urlBase.String(),
298 "error": errExist3, "status": respExist.StatusCode})
299 return fmt.Errorf("http head from url: file does not exist here, aborting: %s, error: %s, status: %d",
300 aURLCommand, errExist2, respExist.StatusCode)
301 }
302 }
303 defer func() {
304 deferredErr := respExist.Body.Close()
305 if deferredErr != nil {
306 logger.Errorw(ctx, "error at closing http head response body", log.Fields{"url": urlBase.String(), "error": deferredErr})
307 }
308 }()
309
310 //trying to download - do it in background as it may take some time ...
311 go func() {
312 req, err2 := http.NewRequest("GET", urlBase.String(), nil)
313 if err2 != nil {
314 logger.Errorw(ctx, "could not generate http request", log.Fields{"url": urlBase.String(), "error": err2})
mpagenko38662d02021-08-11 09:45:19 +0000315 dm.removeImage(ctx, aFileName, false) //wo FileSystem access
mpagenkoc26d4c02021-05-06 14:27:57 +0000316 return
317 }
318 ctx, cancel := context.WithDeadline(ctx, time.Now().Add(dm.dlToAdapterTimeout)) //timeout as given from SetDownloadTimeout()
319 defer cancel()
320 _ = req.WithContext(ctx)
321 resp, err3 := http.DefaultClient.Do(req)
322 if err3 != nil || respExist.StatusCode != http.StatusOK {
323 logger.Errorw(ctx, "could not http get from url", log.Fields{"url": urlBase.String(),
324 "error": err3, "status": respExist.StatusCode})
mpagenko38662d02021-08-11 09:45:19 +0000325 dm.removeImage(ctx, aFileName, false) //wo FileSystem access
mpagenkoc26d4c02021-05-06 14:27:57 +0000326 return
327 }
328 defer func() {
329 deferredErr := resp.Body.Close()
330 if deferredErr != nil {
331 logger.Errorw(ctx, "error at closing http get response body", log.Fields{"url": urlBase.String(), "error": deferredErr})
332 }
333 }()
334
335 // Create the file
336 aLocalPathName := aFilePath + "/" + aFileName
337 file, err := os.Create(aLocalPathName)
338 if err != nil {
339 logger.Errorw(ctx, "could not create local file", log.Fields{"path_file": aLocalPathName, "error": err})
mpagenko38662d02021-08-11 09:45:19 +0000340 dm.removeImage(ctx, aFileName, false) //wo FileSystem access
mpagenkoc26d4c02021-05-06 14:27:57 +0000341 return
342 }
343 defer func() {
344 deferredErr := file.Close()
345 if deferredErr != nil {
346 logger.Errorw(ctx, "error at closing new file", log.Fields{"path_file": aLocalPathName, "error": deferredErr})
347 }
348 }()
349
350 // Write the body to file
351 _, err = io.Copy(file, resp.Body)
352 if err != nil {
353 logger.Errorw(ctx, "could not copy file content", log.Fields{"url": urlBase.String(), "file": aLocalPathName, "error": err})
mpagenko38662d02021-08-11 09:45:19 +0000354 dm.removeImage(ctx, aFileName, true)
mpagenkoc26d4c02021-05-06 14:27:57 +0000355 return
356 }
357
358 fileStats, statsErr := file.Stat()
359 if err != nil {
360 logger.Errorw(ctx, "created file can't be accessed", log.Fields{"file": aLocalPathName, "stat-error": statsErr})
mpagenko5b5cb982021-08-18 16:38:51 +0000361 return
mpagenkoc26d4c02021-05-06 14:27:57 +0000362 }
363 fileSize := fileStats.Size()
364 logger.Infow(ctx, "written file size is", log.Fields{"file": aLocalPathName, "length": fileSize})
365
mpagenko5b5cb982021-08-18 16:38:51 +0000366 dm.updateFileState(ctx, aFileName, fileSize)
mpagenkoc26d4c02021-05-06 14:27:57 +0000367 //TODO:!!! further extension could be provided here, e.g. already computing and possibly comparing the CRC, vendor check
368 }()
369 return nil
370}
mpagenkoaa3afe92021-05-21 16:20:58 +0000371
mpagenko38662d02021-08-11 09:45:19 +0000372//removeImage deletes the given image according to the Image name from filesystem and downloadImageDscSlice
373func (dm *fileDownloadManager) removeImage(ctx context.Context, aImageName string, aDelFs bool) {
374 logger.Debugw(ctx, "remove the image from Adapter", log.Fields{"image-name": aImageName})
mpagenkoaa3afe92021-05-21 16:20:58 +0000375 dm.mutexDownloadImageDsc.RLock()
376 defer dm.mutexDownloadImageDsc.RUnlock()
377
378 tmpSlice := dm.downloadImageDscSlice[:0]
379 for _, dnldImgDsc := range dm.downloadImageDscSlice {
380 if dnldImgDsc.downloadImageName == aImageName {
mpagenko38662d02021-08-11 09:45:19 +0000381 //image found (by name)
mpagenkoaa3afe92021-05-21 16:20:58 +0000382 logger.Debugw(ctx, "removing image", log.Fields{"image-name": aImageName})
mpagenko38662d02021-08-11 09:45:19 +0000383 if aDelFs {
384 //remove the image from filesystem
385 aLocalPathName := cDefaultLocalDir + "/" + aImageName
386 if err := os.Remove(aLocalPathName); err != nil {
387 // might be a temporary situation, when the file was not yet (completely) written
388 logger.Debugw(ctx, "image not removed from filesystem", log.Fields{
389 "image-name": aImageName, "error": err})
390 }
mpagenkoaa3afe92021-05-21 16:20:58 +0000391 }
mpagenko38662d02021-08-11 09:45:19 +0000392 // and remove from the imageDsc slice by just not appending
mpagenkoaa3afe92021-05-21 16:20:58 +0000393 } else {
394 tmpSlice = append(tmpSlice, dnldImgDsc)
395 }
396 }
397 dm.downloadImageDscSlice = tmpSlice
398 //image not found (by name)
399}
mpagenko38662d02021-08-11 09:45:19 +0000400
401//CancelDownload just wraps removeImage (for possible extensions to e.g. really stop a ongoing download)
402func (dm *fileDownloadManager) CancelDownload(ctx context.Context, aImageName string) {
403 // for the moment that would only support to wait for the download end and remove the image then
404 // further reactions while still downloading can be considered with some effort, but does it make sense (synchronous load here!)
405 dm.removeImage(ctx, aImageName, true)
406}