github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/event/target/mqtt.go (about) 1 // Copyright (c) 2015-2023 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package target 19 20 import ( 21 "crypto/tls" 22 "crypto/x509" 23 "encoding/json" 24 "errors" 25 "fmt" 26 "net/url" 27 "os" 28 "path/filepath" 29 "time" 30 31 mqtt "github.com/eclipse/paho.mqtt.golang" 32 "github.com/minio/minio/internal/event" 33 "github.com/minio/minio/internal/logger" 34 "github.com/minio/minio/internal/once" 35 "github.com/minio/minio/internal/store" 36 xnet "github.com/minio/pkg/v2/net" 37 ) 38 39 const ( 40 reconnectInterval = 5 * time.Second 41 storePrefix = "minio" 42 ) 43 44 // MQTT input constants 45 const ( 46 MqttBroker = "broker" 47 MqttTopic = "topic" 48 MqttQoS = "qos" 49 MqttUsername = "username" 50 MqttPassword = "password" 51 MqttReconnectInterval = "reconnect_interval" 52 MqttKeepAliveInterval = "keep_alive_interval" 53 MqttQueueDir = "queue_dir" 54 MqttQueueLimit = "queue_limit" 55 56 EnvMQTTEnable = "MINIO_NOTIFY_MQTT_ENABLE" 57 EnvMQTTBroker = "MINIO_NOTIFY_MQTT_BROKER" 58 EnvMQTTTopic = "MINIO_NOTIFY_MQTT_TOPIC" 59 EnvMQTTQoS = "MINIO_NOTIFY_MQTT_QOS" 60 EnvMQTTUsername = "MINIO_NOTIFY_MQTT_USERNAME" 61 EnvMQTTPassword = "MINIO_NOTIFY_MQTT_PASSWORD" 62 EnvMQTTReconnectInterval = "MINIO_NOTIFY_MQTT_RECONNECT_INTERVAL" 63 EnvMQTTKeepAliveInterval = "MINIO_NOTIFY_MQTT_KEEP_ALIVE_INTERVAL" 64 EnvMQTTQueueDir = "MINIO_NOTIFY_MQTT_QUEUE_DIR" 65 EnvMQTTQueueLimit = "MINIO_NOTIFY_MQTT_QUEUE_LIMIT" 66 ) 67 68 // MQTTArgs - MQTT target arguments. 69 type MQTTArgs struct { 70 Enable bool `json:"enable"` 71 Broker xnet.URL `json:"broker"` 72 Topic string `json:"topic"` 73 QoS byte `json:"qos"` 74 User string `json:"username"` 75 Password string `json:"password"` 76 MaxReconnectInterval time.Duration `json:"reconnectInterval"` 77 KeepAlive time.Duration `json:"keepAliveInterval"` 78 RootCAs *x509.CertPool `json:"-"` 79 QueueDir string `json:"queueDir"` 80 QueueLimit uint64 `json:"queueLimit"` 81 } 82 83 // Validate MQTTArgs fields 84 func (m MQTTArgs) Validate() error { 85 if !m.Enable { 86 return nil 87 } 88 u, err := xnet.ParseURL(m.Broker.String()) 89 if err != nil { 90 return err 91 } 92 switch u.Scheme { 93 case "ws", "wss", "tcp", "ssl", "tls", "tcps": 94 default: 95 return errors.New("unknown protocol in broker address") 96 } 97 if m.QueueDir != "" { 98 if !filepath.IsAbs(m.QueueDir) { 99 return errors.New("queueDir path should be absolute") 100 } 101 if m.QoS == 0 { 102 return errors.New("qos should be set to 1 or 2 if queueDir is set") 103 } 104 } 105 106 return nil 107 } 108 109 // MQTTTarget - MQTT target. 110 type MQTTTarget struct { 111 initOnce once.Init 112 113 id event.TargetID 114 args MQTTArgs 115 client mqtt.Client 116 store store.Store[event.Event] 117 quitCh chan struct{} 118 loggerOnce logger.LogOnce 119 } 120 121 // ID - returns target ID. 122 func (target *MQTTTarget) ID() event.TargetID { 123 return target.id 124 } 125 126 // Name - returns the Name of the target. 127 func (target *MQTTTarget) Name() string { 128 return target.ID().String() 129 } 130 131 // Store returns any underlying store if set. 132 func (target *MQTTTarget) Store() event.TargetStore { 133 return target.store 134 } 135 136 // IsActive - Return true if target is up and active 137 func (target *MQTTTarget) IsActive() (bool, error) { 138 if err := target.init(); err != nil { 139 return false, err 140 } 141 return target.isActive() 142 } 143 144 func (target *MQTTTarget) isActive() (bool, error) { 145 if !target.client.IsConnectionOpen() { 146 return false, store.ErrNotConnected 147 } 148 return true, nil 149 } 150 151 // send - sends an event to the mqtt. 152 func (target *MQTTTarget) send(eventData event.Event) error { 153 objectName, err := url.QueryUnescape(eventData.S3.Object.Key) 154 if err != nil { 155 return err 156 } 157 key := eventData.S3.Bucket.Name + "/" + objectName 158 159 data, err := json.Marshal(event.Log{EventName: eventData.EventName, Key: key, Records: []event.Event{eventData}}) 160 if err != nil { 161 return err 162 } 163 164 token := target.client.Publish(target.args.Topic, target.args.QoS, false, string(data)) 165 if !token.WaitTimeout(reconnectInterval) { 166 return store.ErrNotConnected 167 } 168 return token.Error() 169 } 170 171 // SendFromStore - reads an event from store and sends it to MQTT. 172 func (target *MQTTTarget) SendFromStore(key store.Key) error { 173 if err := target.init(); err != nil { 174 return err 175 } 176 177 // Do not send if the connection is not active. 178 _, err := target.isActive() 179 if err != nil { 180 return err 181 } 182 183 eventData, err := target.store.Get(key.Name) 184 if err != nil { 185 // The last event key in a successful batch will be sent in the channel atmost once by the replayEvents() 186 // Such events will not exist and wouldve been already been sent successfully. 187 if os.IsNotExist(err) { 188 return nil 189 } 190 return err 191 } 192 193 if err = target.send(eventData); err != nil { 194 return err 195 } 196 197 // Delete the event from store. 198 return target.store.Del(key.Name) 199 } 200 201 // Save - saves the events to the store if queuestore is configured, which will 202 // be replayed when the mqtt connection is active. 203 func (target *MQTTTarget) Save(eventData event.Event) error { 204 if target.store != nil { 205 return target.store.Put(eventData) 206 } 207 if err := target.init(); err != nil { 208 return err 209 } 210 211 // Do not send if the connection is not active. 212 _, err := target.isActive() 213 if err != nil { 214 return err 215 } 216 217 return target.send(eventData) 218 } 219 220 // Close - does nothing and available for interface compatibility. 221 func (target *MQTTTarget) Close() error { 222 if target.client != nil { 223 target.client.Disconnect(100) 224 } 225 close(target.quitCh) 226 return nil 227 } 228 229 func (target *MQTTTarget) init() error { 230 return target.initOnce.Do(target.initMQTT) 231 } 232 233 func (target *MQTTTarget) initMQTT() error { 234 args := target.args 235 236 // Using hex here, to make sure we avoid 23 237 // character limit on client_id according to 238 // MQTT spec. 239 clientID := fmt.Sprintf("%x", time.Now().UnixNano()) 240 241 options := mqtt.NewClientOptions(). 242 SetClientID(clientID). 243 SetCleanSession(true). 244 SetUsername(args.User). 245 SetPassword(args.Password). 246 SetMaxReconnectInterval(args.MaxReconnectInterval). 247 SetKeepAlive(args.KeepAlive). 248 SetTLSConfig(&tls.Config{RootCAs: args.RootCAs}). 249 AddBroker(args.Broker.String()) 250 251 target.client = mqtt.NewClient(options) 252 253 token := target.client.Connect() 254 ok := token.WaitTimeout(reconnectInterval) 255 if !ok { 256 return store.ErrNotConnected 257 } 258 if token.Error() != nil { 259 return token.Error() 260 } 261 262 yes, err := target.isActive() 263 if err != nil { 264 return err 265 } 266 if !yes { 267 return store.ErrNotConnected 268 } 269 270 return nil 271 } 272 273 // NewMQTTTarget - creates new MQTT target. 274 func NewMQTTTarget(id string, args MQTTArgs, loggerOnce logger.LogOnce) (*MQTTTarget, error) { 275 if args.MaxReconnectInterval == 0 { 276 // Default interval 277 // https://github.com/eclipse/paho.mqtt.golang/blob/master/options.go#L115 278 args.MaxReconnectInterval = 10 * time.Minute 279 } 280 281 if args.KeepAlive == 0 { 282 args.KeepAlive = 10 * time.Second 283 } 284 285 var queueStore store.Store[event.Event] 286 if args.QueueDir != "" { 287 queueDir := filepath.Join(args.QueueDir, storePrefix+"-mqtt-"+id) 288 queueStore = store.NewQueueStore[event.Event](queueDir, args.QueueLimit, event.StoreExtension) 289 if err := queueStore.Open(); err != nil { 290 return nil, fmt.Errorf("unable to initialize the queue store of MQTT `%s`: %w", id, err) 291 } 292 } 293 294 target := &MQTTTarget{ 295 id: event.TargetID{ID: id, Name: "mqtt"}, 296 args: args, 297 store: queueStore, 298 quitCh: make(chan struct{}), 299 loggerOnce: loggerOnce, 300 } 301 302 if target.store != nil { 303 store.StreamItems(target.store, target, target.quitCh, target.loggerOnce) 304 } 305 306 return target, nil 307 }