blob: 1dfd50aba9c21dce247e72261c49266bd0aa2d40 [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"
28 "sync"
29 "time"
30
31 "github.com/opencord/voltha-lib-go/v4/pkg/log"
mpagenkoaa3afe92021-05-21 16:20:58 +000032 "github.com/opencord/voltha-protos/v4/go/voltha"
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}
115 dm.mutexDownloadImageDsc.Lock()
116 dm.downloadImageDscSlice = append(dm.downloadImageDscSlice, loDownloadImageParams)
117 dm.mutexDownloadImageDsc.Unlock()
118 //try to download from http
119 err := dm.downloadFile(ctx, aURLCommand, cDefaultLocalDir, aImageName)
120 //return the result of the start-request to comfort the core processing even though the complete download may go on in background
121 return err
122}
123
124//GetImageBufferLen returns the length of the specified file in bytes (file size) - as detected after download
125func (dm *fileDownloadManager) GetImageBufferLen(ctx context.Context, aFileName string) (int64, error) {
126 dm.mutexDownloadImageDsc.RLock()
127 defer dm.mutexDownloadImageDsc.RUnlock()
128 for _, dnldImgDsc := range dm.downloadImageDscSlice {
129 if dnldImgDsc.downloadImageName == aFileName && dnldImgDsc.downloadImageState == cFileStateDlSucceeded {
130 //image found (by name) and fully downloaded
131 return dnldImgDsc.downloadImageLen, nil
132 }
133 }
134 return 0, fmt.Errorf("no downloaded image found: %s", aFileName)
135}
136
137//GetDownloadImageBuffer returns the content of the requested file as byte slice
138func (dm *fileDownloadManager) GetDownloadImageBuffer(ctx context.Context, aFileName string) ([]byte, error) {
139 //nolint:gosec
140 file, err := os.Open(cDefaultLocalDir + "/" + aFileName)
141 if err != nil {
142 return nil, err
143 }
144 //nolint:errcheck
145 defer file.Close()
146
147 stats, statsErr := file.Stat()
148 if statsErr != nil {
149 return nil, statsErr
150 }
151
152 var size int64 = stats.Size()
153 bytes := make([]byte, size)
154
155 buffer := bufio.NewReader(file)
156 _, err = buffer.Read(bytes)
157
158 return bytes, err
159}
160
161//RequestDownloadReady receives a channel that has to be used to inform the requester in case the concerned file is downloaded
162func (dm *fileDownloadManager) RequestDownloadReady(ctx context.Context, aFileName string, aWaitChannel chan<- bool) {
163 if dm.imageLocallyDownloaded(ctx, aFileName) {
164 //image found (by name) and fully downloaded
165 logger.Debugw(ctx, "file ready - immediate response", log.Fields{"image-name": aFileName})
166 aWaitChannel <- true
167 return
168 }
169 //when we are here the image was not yet found or not fully downloaded -
170 // add the device specific channel to the list of waiting requesters
171 dm.mutexDownloadImageDsc.Lock()
172 defer dm.mutexDownloadImageDsc.Unlock()
173 if loRequesterChannelMap, ok := dm.dnldImgReadyWaiting[aFileName]; ok {
174 //entry for the file name already exists
175 if _, exists := loRequesterChannelMap[aWaitChannel]; !exists {
176 // requester channel does not yet exist for the image
177 loRequesterChannelMap[aWaitChannel] = struct{}{}
178 dm.dnldImgReadyWaiting[aFileName] = loRequesterChannelMap
179 logger.Debugw(ctx, "file not ready - adding new requester", log.Fields{
180 "image-name": aFileName, "number-of-requesters": len(dm.dnldImgReadyWaiting[aFileName])})
181 }
182 } else {
183 //entry for the file name does not even exist
184 addRequesterChannelMap := make(map[chan<- bool]struct{})
185 addRequesterChannelMap[aWaitChannel] = struct{}{}
186 dm.dnldImgReadyWaiting[aFileName] = addRequesterChannelMap
187 logger.Debugw(ctx, "file not ready - setting first requester", log.Fields{
188 "image-name": aFileName})
189 }
190}
191
192//RemoveReadyRequest removes the specified channel from the requester(channel) map for the given file name
193func (dm *fileDownloadManager) RemoveReadyRequest(ctx context.Context, aFileName string, aWaitChannel chan bool) {
194 dm.mutexDownloadImageDsc.Lock()
195 defer dm.mutexDownloadImageDsc.Unlock()
196 for imageName, channelMap := range dm.dnldImgReadyWaiting {
197 if imageName == aFileName {
198 for channel := range channelMap {
199 if channel == aWaitChannel {
200 delete(dm.dnldImgReadyWaiting[imageName], channel)
201 logger.Debugw(ctx, "channel removed from the requester map", log.Fields{
202 "image-name": aFileName, "new number-of-requesters": len(dm.dnldImgReadyWaiting[aFileName])})
203 return //can leave directly
204 }
205 }
206 return //can leave directly
207 }
208 }
209}
210
211// FileDownloadManager private (unexported) methods -- start
212
213//imageLocallyDownloaded returns true if the requested image already exists within the adapter
214func (dm *fileDownloadManager) imageLocallyDownloaded(ctx context.Context, aImageName string) bool {
215 logger.Debugw(ctx, "checking if image is fully downloaded to adapter", log.Fields{"image-name": aImageName})
216 dm.mutexDownloadImageDsc.RLock()
217 defer dm.mutexDownloadImageDsc.RUnlock()
218
219 for _, dnldImgDsc := range dm.downloadImageDscSlice {
220 if dnldImgDsc.downloadImageName == aImageName {
221 //image found (by name)
222 if dnldImgDsc.downloadImageState == cFileStateDlSucceeded {
223 logger.Debugw(ctx, "image has been fully downloaded", log.Fields{"image-name": aImageName})
224 return true
225 }
226 logger.Debugw(ctx, "image not yet fully downloaded", log.Fields{"image-name": aImageName})
227 return false
228 }
229 }
230 //image not found (by name)
231 logger.Errorw(ctx, "image does not exist", log.Fields{"image-name": aImageName})
232 return false
233}
234
235//downloadFile downloads the specified file from the given http location
236func (dm *fileDownloadManager) downloadFile(ctx context.Context, aURLCommand string, aFilePath string, aFileName string) error {
237 // Get the data
238 logger.Infow(ctx, "downloading with URL", log.Fields{"url": aURLCommand, "localPath": aFilePath})
239 // verifying the complete URL by parsing it to its URL elements
240 urlBase, err1 := url.Parse(aURLCommand)
241 if err1 != nil {
242 logger.Errorw(ctx, "could not set base url command", log.Fields{"url": aURLCommand, "error": err1})
243 return fmt.Errorf("could not set base url command: %s, error: %s", aURLCommand, err1)
244 }
245 urlParams := url.Values{}
246 urlBase.RawQuery = urlParams.Encode()
247
248 //pre-check on file existence - assuming http location here
249 reqExist, errExist2 := http.NewRequest("HEAD", urlBase.String(), nil)
250 if errExist2 != nil {
251 logger.Errorw(ctx, "could not generate http head request", log.Fields{"url": urlBase.String(), "error": errExist2})
252 return fmt.Errorf("could not generate http head request: %s, error: %s", aURLCommand, errExist2)
253 }
254 ctxExist, cancelExist := context.WithDeadline(ctx, time.Now().Add(3*time.Second)) //waiting for some fast answer
255 defer cancelExist()
256 _ = reqExist.WithContext(ctxExist)
257 respExist, errExist3 := http.DefaultClient.Do(reqExist)
258 if errExist3 != nil || respExist.StatusCode != http.StatusOK {
259 logger.Infow(ctx, "could not http head from url", log.Fields{"url": urlBase.String(),
260 "error": errExist3, "status": respExist.StatusCode})
261 //if head is not supported by server we cannot use this test and just try to continue
262 if respExist.StatusCode != http.StatusMethodNotAllowed {
263 logger.Errorw(ctx, "http head from url: file does not exist here, aborting", log.Fields{"url": urlBase.String(),
264 "error": errExist3, "status": respExist.StatusCode})
265 return fmt.Errorf("http head from url: file does not exist here, aborting: %s, error: %s, status: %d",
266 aURLCommand, errExist2, respExist.StatusCode)
267 }
268 }
269 defer func() {
270 deferredErr := respExist.Body.Close()
271 if deferredErr != nil {
272 logger.Errorw(ctx, "error at closing http head response body", log.Fields{"url": urlBase.String(), "error": deferredErr})
273 }
274 }()
275
276 //trying to download - do it in background as it may take some time ...
277 go func() {
278 req, err2 := http.NewRequest("GET", urlBase.String(), nil)
279 if err2 != nil {
280 logger.Errorw(ctx, "could not generate http request", log.Fields{"url": urlBase.String(), "error": err2})
281 return
282 }
283 ctx, cancel := context.WithDeadline(ctx, time.Now().Add(dm.dlToAdapterTimeout)) //timeout as given from SetDownloadTimeout()
284 defer cancel()
285 _ = req.WithContext(ctx)
286 resp, err3 := http.DefaultClient.Do(req)
287 if err3 != nil || respExist.StatusCode != http.StatusOK {
288 logger.Errorw(ctx, "could not http get from url", log.Fields{"url": urlBase.String(),
289 "error": err3, "status": respExist.StatusCode})
290 return
291 }
292 defer func() {
293 deferredErr := resp.Body.Close()
294 if deferredErr != nil {
295 logger.Errorw(ctx, "error at closing http get response body", log.Fields{"url": urlBase.String(), "error": deferredErr})
296 }
297 }()
298
299 // Create the file
300 aLocalPathName := aFilePath + "/" + aFileName
301 file, err := os.Create(aLocalPathName)
302 if err != nil {
303 logger.Errorw(ctx, "could not create local file", log.Fields{"path_file": aLocalPathName, "error": err})
304 return
305 }
306 defer func() {
307 deferredErr := file.Close()
308 if deferredErr != nil {
309 logger.Errorw(ctx, "error at closing new file", log.Fields{"path_file": aLocalPathName, "error": deferredErr})
310 }
311 }()
312
313 // Write the body to file
314 _, err = io.Copy(file, resp.Body)
315 if err != nil {
316 logger.Errorw(ctx, "could not copy file content", log.Fields{"url": urlBase.String(), "file": aLocalPathName, "error": err})
317 return
318 }
319
320 fileStats, statsErr := file.Stat()
321 if err != nil {
322 logger.Errorw(ctx, "created file can't be accessed", log.Fields{"file": aLocalPathName, "stat-error": statsErr})
323 }
324 fileSize := fileStats.Size()
325 logger.Infow(ctx, "written file size is", log.Fields{"file": aLocalPathName, "length": fileSize})
326
327 dm.mutexDownloadImageDsc.Lock()
328 defer dm.mutexDownloadImageDsc.Unlock()
329 for imgKey, dnldImgDsc := range dm.downloadImageDscSlice {
330 if dnldImgDsc.downloadImageName == aFileName {
331 //image found (by name) - need to write changes on the original map
332 dm.downloadImageDscSlice[imgKey].downloadImageState = cFileStateDlSucceeded
333 dm.downloadImageDscSlice[imgKey].downloadImageLen = fileSize
334 //in case upgrade process(es) was/were waiting for the file, inform them
335 for imageName, channelMap := range dm.dnldImgReadyWaiting {
336 if imageName == aFileName {
337 for channel := range channelMap {
338 // use all found channels to inform possible requesters about the existence of the file
339 channel <- true
340 delete(dm.dnldImgReadyWaiting[imageName], channel) //requester served
341 }
342 return //can leave directly
343 }
344 }
345 return //can leave directly
346 }
347 }
348 //TODO:!!! further extension could be provided here, e.g. already computing and possibly comparing the CRC, vendor check
349 }()
350 return nil
351}
mpagenkoaa3afe92021-05-21 16:20:58 +0000352
353func (dm *fileDownloadManager) RequestDownloadState(ctx context.Context, aImageName string,
354 apDlToAdapterImageState *voltha.ImageState) {
355 logger.Debugw(ctx, "request download state for image to Adapter", log.Fields{"image-name": aImageName})
356 dm.mutexDownloadImageDsc.RLock()
357 defer dm.mutexDownloadImageDsc.RUnlock()
358
359 for _, dnldImgDsc := range dm.downloadImageDscSlice {
360 if dnldImgDsc.downloadImageName == aImageName {
361 //image found (by name)
362 apDlToAdapterImageState.DownloadState = voltha.ImageState_DOWNLOAD_REQUESTED
363 apDlToAdapterImageState.Reason = voltha.ImageState_NO_ERROR
364 return
365 }
366 }
367 //image not found (by name)
368 apDlToAdapterImageState.DownloadState = voltha.ImageState_DOWNLOAD_UNKNOWN
369 apDlToAdapterImageState.Reason = voltha.ImageState_NO_ERROR
370}
371
372func (dm *fileDownloadManager) CancelDownload(ctx context.Context, aImageName string) {
373 logger.Debugw(ctx, "Cancel the download to Adapter", log.Fields{"image-name": aImageName})
374 // for the moment that would only support to wait for the download end and remove the image then
375 // further reactions while still downloading can be considered with some effort, but does it make sense (synchronous load here!)
376 dm.mutexDownloadImageDsc.RLock()
377 defer dm.mutexDownloadImageDsc.RUnlock()
378
379 tmpSlice := dm.downloadImageDscSlice[:0]
380 for _, dnldImgDsc := range dm.downloadImageDscSlice {
381 if dnldImgDsc.downloadImageName == aImageName {
382 //image found (by name) - remove the image from filesystem
383 logger.Debugw(ctx, "removing image", log.Fields{"image-name": aImageName})
384 aLocalPathName := cDefaultLocalDir + "/" + aImageName
385 if err := os.Remove(aLocalPathName); err != nil {
386 logger.Debugw(ctx, "image not removed from filesystem", log.Fields{
387 "image-name": aImageName, "error": err})
388 }
389 // and in the imageDsc slice by just not appending
390 } else {
391 tmpSlice = append(tmpSlice, dnldImgDsc)
392 }
393 }
394 dm.downloadImageDscSlice = tmpSlice
395 //image not found (by name)
396}