blob: 3767df2c6ccd93f301a41d1085a84afd0596f963 [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) {
168 if dm.imageLocallyDownloaded(ctx, aFileName) {
169 //image found (by name) and fully downloaded
170 logger.Debugw(ctx, "file ready - immediate response", log.Fields{"image-name": aFileName})
171 aWaitChannel <- true
172 return
173 }
174 //when we are here the image was not yet found or not fully downloaded -
175 // add the device specific channel to the list of waiting requesters
176 dm.mutexDownloadImageDsc.Lock()
177 defer dm.mutexDownloadImageDsc.Unlock()
178 if loRequesterChannelMap, ok := dm.dnldImgReadyWaiting[aFileName]; ok {
179 //entry for the file name already exists
180 if _, exists := loRequesterChannelMap[aWaitChannel]; !exists {
181 // requester channel does not yet exist for the image
182 loRequesterChannelMap[aWaitChannel] = struct{}{}
183 dm.dnldImgReadyWaiting[aFileName] = loRequesterChannelMap
184 logger.Debugw(ctx, "file not ready - adding new requester", log.Fields{
185 "image-name": aFileName, "number-of-requesters": len(dm.dnldImgReadyWaiting[aFileName])})
186 }
187 } else {
188 //entry for the file name does not even exist
189 addRequesterChannelMap := make(map[chan<- bool]struct{})
190 addRequesterChannelMap[aWaitChannel] = struct{}{}
191 dm.dnldImgReadyWaiting[aFileName] = addRequesterChannelMap
192 logger.Debugw(ctx, "file not ready - setting first requester", log.Fields{
193 "image-name": aFileName})
194 }
195}
196
197//RemoveReadyRequest removes the specified channel from the requester(channel) map for the given file name
198func (dm *fileDownloadManager) RemoveReadyRequest(ctx context.Context, aFileName string, aWaitChannel chan bool) {
199 dm.mutexDownloadImageDsc.Lock()
200 defer dm.mutexDownloadImageDsc.Unlock()
201 for imageName, channelMap := range dm.dnldImgReadyWaiting {
202 if imageName == aFileName {
203 for channel := range channelMap {
204 if channel == aWaitChannel {
205 delete(dm.dnldImgReadyWaiting[imageName], channel)
206 logger.Debugw(ctx, "channel removed from the requester map", log.Fields{
207 "image-name": aFileName, "new number-of-requesters": len(dm.dnldImgReadyWaiting[aFileName])})
208 return //can leave directly
209 }
210 }
211 return //can leave directly
212 }
213 }
214}
215
216// FileDownloadManager private (unexported) methods -- start
217
218//imageLocallyDownloaded returns true if the requested image already exists within the adapter
219func (dm *fileDownloadManager) imageLocallyDownloaded(ctx context.Context, aImageName string) bool {
220 logger.Debugw(ctx, "checking if image is fully downloaded to adapter", log.Fields{"image-name": aImageName})
221 dm.mutexDownloadImageDsc.RLock()
222 defer dm.mutexDownloadImageDsc.RUnlock()
223
224 for _, dnldImgDsc := range dm.downloadImageDscSlice {
225 if dnldImgDsc.downloadImageName == aImageName {
226 //image found (by name)
227 if dnldImgDsc.downloadImageState == cFileStateDlSucceeded {
228 logger.Debugw(ctx, "image has been fully downloaded", log.Fields{"image-name": aImageName})
229 return true
230 }
231 logger.Debugw(ctx, "image not yet fully downloaded", log.Fields{"image-name": aImageName})
232 return false
233 }
234 }
235 //image not found (by name)
236 logger.Errorw(ctx, "image does not exist", log.Fields{"image-name": aImageName})
237 return false
238}
239
240//downloadFile downloads the specified file from the given http location
241func (dm *fileDownloadManager) downloadFile(ctx context.Context, aURLCommand string, aFilePath string, aFileName string) error {
242 // Get the data
243 logger.Infow(ctx, "downloading with URL", log.Fields{"url": aURLCommand, "localPath": aFilePath})
244 // verifying the complete URL by parsing it to its URL elements
245 urlBase, err1 := url.Parse(aURLCommand)
246 if err1 != nil {
247 logger.Errorw(ctx, "could not set base url command", log.Fields{"url": aURLCommand, "error": err1})
248 return fmt.Errorf("could not set base url command: %s, error: %s", aURLCommand, err1)
249 }
250 urlParams := url.Values{}
251 urlBase.RawQuery = urlParams.Encode()
252
253 //pre-check on file existence - assuming http location here
254 reqExist, errExist2 := http.NewRequest("HEAD", urlBase.String(), nil)
255 if errExist2 != nil {
256 logger.Errorw(ctx, "could not generate http head request", log.Fields{"url": urlBase.String(), "error": errExist2})
257 return fmt.Errorf("could not generate http head request: %s, error: %s", aURLCommand, errExist2)
258 }
259 ctxExist, cancelExist := context.WithDeadline(ctx, time.Now().Add(3*time.Second)) //waiting for some fast answer
260 defer cancelExist()
261 _ = reqExist.WithContext(ctxExist)
262 respExist, errExist3 := http.DefaultClient.Do(reqExist)
263 if errExist3 != nil || respExist.StatusCode != http.StatusOK {
264 logger.Infow(ctx, "could not http head from url", log.Fields{"url": urlBase.String(),
265 "error": errExist3, "status": respExist.StatusCode})
266 //if head is not supported by server we cannot use this test and just try to continue
267 if respExist.StatusCode != http.StatusMethodNotAllowed {
268 logger.Errorw(ctx, "http head from url: file does not exist here, aborting", log.Fields{"url": urlBase.String(),
269 "error": errExist3, "status": respExist.StatusCode})
270 return fmt.Errorf("http head from url: file does not exist here, aborting: %s, error: %s, status: %d",
271 aURLCommand, errExist2, respExist.StatusCode)
272 }
273 }
274 defer func() {
275 deferredErr := respExist.Body.Close()
276 if deferredErr != nil {
277 logger.Errorw(ctx, "error at closing http head response body", log.Fields{"url": urlBase.String(), "error": deferredErr})
278 }
279 }()
280
281 //trying to download - do it in background as it may take some time ...
282 go func() {
283 req, err2 := http.NewRequest("GET", urlBase.String(), nil)
284 if err2 != nil {
285 logger.Errorw(ctx, "could not generate http request", log.Fields{"url": urlBase.String(), "error": err2})
mpagenko38662d02021-08-11 09:45:19 +0000286 dm.removeImage(ctx, aFileName, false) //wo FileSystem access
mpagenkoc26d4c02021-05-06 14:27:57 +0000287 return
288 }
289 ctx, cancel := context.WithDeadline(ctx, time.Now().Add(dm.dlToAdapterTimeout)) //timeout as given from SetDownloadTimeout()
290 defer cancel()
291 _ = req.WithContext(ctx)
292 resp, err3 := http.DefaultClient.Do(req)
293 if err3 != nil || respExist.StatusCode != http.StatusOK {
294 logger.Errorw(ctx, "could not http get from url", log.Fields{"url": urlBase.String(),
295 "error": err3, "status": respExist.StatusCode})
mpagenko38662d02021-08-11 09:45:19 +0000296 dm.removeImage(ctx, aFileName, false) //wo FileSystem access
mpagenkoc26d4c02021-05-06 14:27:57 +0000297 return
298 }
299 defer func() {
300 deferredErr := resp.Body.Close()
301 if deferredErr != nil {
302 logger.Errorw(ctx, "error at closing http get response body", log.Fields{"url": urlBase.String(), "error": deferredErr})
303 }
304 }()
305
306 // Create the file
307 aLocalPathName := aFilePath + "/" + aFileName
308 file, err := os.Create(aLocalPathName)
309 if err != nil {
310 logger.Errorw(ctx, "could not create local file", log.Fields{"path_file": aLocalPathName, "error": err})
mpagenko38662d02021-08-11 09:45:19 +0000311 dm.removeImage(ctx, aFileName, false) //wo FileSystem access
mpagenkoc26d4c02021-05-06 14:27:57 +0000312 return
313 }
314 defer func() {
315 deferredErr := file.Close()
316 if deferredErr != nil {
317 logger.Errorw(ctx, "error at closing new file", log.Fields{"path_file": aLocalPathName, "error": deferredErr})
318 }
319 }()
320
321 // Write the body to file
322 _, err = io.Copy(file, resp.Body)
323 if err != nil {
324 logger.Errorw(ctx, "could not copy file content", log.Fields{"url": urlBase.String(), "file": aLocalPathName, "error": err})
mpagenko38662d02021-08-11 09:45:19 +0000325 dm.removeImage(ctx, aFileName, true)
mpagenkoc26d4c02021-05-06 14:27:57 +0000326 return
327 }
328
329 fileStats, statsErr := file.Stat()
330 if err != nil {
331 logger.Errorw(ctx, "created file can't be accessed", log.Fields{"file": aLocalPathName, "stat-error": statsErr})
332 }
333 fileSize := fileStats.Size()
334 logger.Infow(ctx, "written file size is", log.Fields{"file": aLocalPathName, "length": fileSize})
335
336 dm.mutexDownloadImageDsc.Lock()
337 defer dm.mutexDownloadImageDsc.Unlock()
338 for imgKey, dnldImgDsc := range dm.downloadImageDscSlice {
339 if dnldImgDsc.downloadImageName == aFileName {
340 //image found (by name) - need to write changes on the original map
341 dm.downloadImageDscSlice[imgKey].downloadImageState = cFileStateDlSucceeded
342 dm.downloadImageDscSlice[imgKey].downloadImageLen = fileSize
343 //in case upgrade process(es) was/were waiting for the file, inform them
344 for imageName, channelMap := range dm.dnldImgReadyWaiting {
345 if imageName == aFileName {
346 for channel := range channelMap {
347 // use all found channels to inform possible requesters about the existence of the file
348 channel <- true
349 delete(dm.dnldImgReadyWaiting[imageName], channel) //requester served
350 }
351 return //can leave directly
352 }
353 }
354 return //can leave directly
355 }
356 }
357 //TODO:!!! further extension could be provided here, e.g. already computing and possibly comparing the CRC, vendor check
358 }()
359 return nil
360}
mpagenkoaa3afe92021-05-21 16:20:58 +0000361
mpagenko38662d02021-08-11 09:45:19 +0000362//removeImage deletes the given image according to the Image name from filesystem and downloadImageDscSlice
363func (dm *fileDownloadManager) removeImage(ctx context.Context, aImageName string, aDelFs bool) {
364 logger.Debugw(ctx, "remove the image from Adapter", log.Fields{"image-name": aImageName})
mpagenkoaa3afe92021-05-21 16:20:58 +0000365 dm.mutexDownloadImageDsc.RLock()
366 defer dm.mutexDownloadImageDsc.RUnlock()
367
368 tmpSlice := dm.downloadImageDscSlice[:0]
369 for _, dnldImgDsc := range dm.downloadImageDscSlice {
370 if dnldImgDsc.downloadImageName == aImageName {
mpagenko38662d02021-08-11 09:45:19 +0000371 //image found (by name)
mpagenkoaa3afe92021-05-21 16:20:58 +0000372 logger.Debugw(ctx, "removing image", log.Fields{"image-name": aImageName})
mpagenko38662d02021-08-11 09:45:19 +0000373 if aDelFs {
374 //remove the image from filesystem
375 aLocalPathName := cDefaultLocalDir + "/" + aImageName
376 if err := os.Remove(aLocalPathName); err != nil {
377 // might be a temporary situation, when the file was not yet (completely) written
378 logger.Debugw(ctx, "image not removed from filesystem", log.Fields{
379 "image-name": aImageName, "error": err})
380 }
mpagenkoaa3afe92021-05-21 16:20:58 +0000381 }
mpagenko38662d02021-08-11 09:45:19 +0000382 // and remove from the imageDsc slice by just not appending
mpagenkoaa3afe92021-05-21 16:20:58 +0000383 } else {
384 tmpSlice = append(tmpSlice, dnldImgDsc)
385 }
386 }
387 dm.downloadImageDscSlice = tmpSlice
388 //image not found (by name)
389}
mpagenko38662d02021-08-11 09:45:19 +0000390
391//CancelDownload just wraps removeImage (for possible extensions to e.g. really stop a ongoing download)
392func (dm *fileDownloadManager) CancelDownload(ctx context.Context, aImageName string) {
393 // for the moment that would only support to wait for the download end and remove the image then
394 // further reactions while still downloading can be considered with some effort, but does it make sense (synchronous load here!)
395 dm.removeImage(ctx, aImageName, true)
396}