1 | | == Developer Documentation == |
2 | | |
3 | | [[TOC(heading=OMF 6.0 Documentation, OMF/OMF60/1Developer*, depth=4)]] |
4 | | |
| 1 | == Developers Guide to OMF == |
| 2 | |
| 3 | === Prerequisites === |
| 4 | ==== Set up your environment ==== |
| 5 | Please please make sure you followed the [wiki:OMF/OMF60/2Operator/0aInstallation INSTALLATION GUIDE] and get things installed and configured properly. |
| 6 | |
| 7 | === Communication layer test and debug tools === |
| 8 | To quickly send a message or monitor incoming messages on a topic, we provide a few utilities that come with the omf_common gem. |
| 9 | |
| 10 | Monitor a topic: |
| 11 | {{{ |
| 12 | $ omf_monitor_topic -h |
| 13 | Usage: omf_monitor_topic [options] topic1 topic2 ... |
| 14 | |
| 15 | Monitor a set of resources (topics) and print all observed messages. |
| 16 | |
| 17 | If the 'follow-children' flag is set, automatically add all resources |
| 18 | created by the monitored resources to the monitor set. Please note |
| 19 | that there will be a delay until the new monitors are in place which |
| 20 | can result in missed messages. |
| 21 | |
| 22 | -c, --comms-url URL URL of communication server (e.g. xmpp://my.server.com) [] |
| 23 | -f, --[no-]follow-children Follow all newly created resources [true] |
| 24 | -d, --debug Set log level to DEBUG |
| 25 | -h, --help Show this message |
| 26 | }}} |
| 27 | |
| 28 | Send a CREATE message: |
| 29 | {{{ |
| 30 | $ omf_send_create -h |
| 31 | Usage: omf_send_create [options] property1:value1 property2:value2 ... |
| 32 | |
| 33 | Send a create message to a specific resource (topic) and print out any replies. |
| 34 | |
| 35 | Any additional command line arguments are interpreted as property:value and are |
| 36 | sent with the create message. |
| 37 | |
| 38 | -r, --resource-url URL URL of resource (e.g. xmpp://my.server.com/topic1) |
| 39 | -t, --type TYPE Type of resource to create (e.g. node) |
| 40 | -y, --yaml YAML_FILE Read type and property from YAML file |
| 41 | -d, --debug Set log level to DEBUG |
| 42 | -h, --help Show this message |
| 43 | }}} |
| 44 | |
| 45 | Send a REQUEST message: |
| 46 | {{{ |
| 47 | $ omf_send_request -h |
| 48 | Usage: omf_send_request [options] prop1 prop2 ... |
| 49 | |
| 50 | Send a request to a specific resource (topic) and print out any replies. |
| 51 | |
| 52 | Any additional command line arguments are interpreted as limiting the request |
| 53 | to those, otherwise all properties are requested. |
| 54 | |
| 55 | -r, --resource-url URL URL of resource (e.g. xmpp://my.server.com/topic1) |
| 56 | -d, --debug Set log level to DEBUG |
| 57 | -h, --help Show this message |
| 58 | }}} |
| 59 | |
| 60 | === Background === |
| 61 | ==== FRCP protocol ==== |
| 62 | If you are more towards implementing FRCP protocol without using Ruby nor our library, you could skip this guide and go to check out [wiki:/OMF/OMF6/1Developer/ArchitecturalFoundation2ProtocolInteractions FRCP protocol documents]. |
| 63 | |
| 64 | ==== Resource controller system ==== |
| 65 | One of the biggest changes we are trying to make in version 6 resource controller system is to focus on the core features, and instead of trying to implement all the functionalities and hardware support, we want to provide an abstract entity acts as the proxy, processing the resource related messages based on the new FRCP protocol, and decides what type of the actions to perform according to the operation defined by the message and constrained by the proxy's capabilities which could be easily defined and extended by the resource providers. |
| 66 | |
| 67 | In our design, users interact with the resource proxies purely via pubsub messages, they publish certain operation (create, request, configure, and release) messages to the pubsub topics, and subscribe to the PubSub topics for inform messages published by the resource proxies based on the outcome of the these requested operations. The resource proxy instances are actually doing the same, but the opposite, they are subscribing to the PubSub topics, react when new operation messages appeared by calling the internal methods corresponding to the content of the operation messages. |
| 68 | |
| 69 | Interaction with proxy |
| 70 | |
| 71 | We created a little tutorial will give a brief example demonstrating the work flow and how to implement these resource proxy definitions. |
| 72 | |
| 73 | === Tutorial === |
| 74 | Suppose we are doing some kind of engine testing in a racing team garage, and using OMF to build a garage controller to handle interaction messages issued by the staff. |
| 75 | |
| 76 | This tutorial will utilise DSL methods provided by our RC library, and the full API documentation can be accessed here: |
| 77 | |
| 78 | [/docs/60/OmfRc/ResourceProxyDSL/ClassMethods.html Resource proxy DSL API doucumentation ] |
| 79 | |
| 80 | This will be a good reference you feel puzzled about certain keywords, methods in the tutorial code we provide here. |
| 81 | |
| 82 | The Ruby source code provided in this tutorial can be copied and pasted to your favourite editor, and execute as standard Ruby scripts. |
| 83 | |
| 84 | ==== Step 1: Garage and engine proxy definition ==== |
| 85 | We will build a garage resource controller acts as the proxy to the garage and engines, while the staff use scripts to interact and guide garage controller to perform certain operations. |
| 86 | |
| 87 | A resource proxy definition module (Mixin) represents the functionalities the resource could provide, including internal property state, what properties can be configured or requested, what operations to perform based on stage of the work flow. The features defined in the proxy definition module will be available to the engine proxy instance by the time it is created by the resource factory. |
| 88 | |
| 89 | We start by defining a garage proxy and a engine proxy, where garage can create engines (parent child relationship). |
| 90 | |
| 91 | ===== Garage controller skeletion ===== |
| 92 | {{{ |
| 93 | # Need omf_rc gem to be required |
| 94 | # |
| 95 | require 'omf_rc' |
| 96 | |
| 97 | # By using default namespace OmfRc::ResourceProxy, the module defined could be loaded automatically. |
| 98 | # |
| 99 | module OmfRc::ResourceProxy::Garage |
| 100 | # Include DSL module, which provides all DSL helper methods |
| 101 | # |
| 102 | include OmfRc::ResourceProxyDSL |
| 103 | |
| 104 | # DSL method register_proxy will register this module definition, |
| 105 | # where :garage become the :type of the proxy. |
| 106 | # |
| 107 | register_proxy :garage |
| 108 | end |
| 109 | |
| 110 | module OmfRc::ResourceProxy::Engine |
| 111 | include OmfRc::ResourceProxyDSL |
| 112 | |
| 113 | # You can specify what kind of proxy can create it, this case, :garage |
| 114 | # |
| 115 | register_proxy :engine, :create_by => :garage |
| 116 | |
| 117 | # DSL method property will define proxy's internal properties, |
| 118 | # and you can provide initial default value. |
| 119 | # |
| 120 | property :manufacturer, :default => "Cosworth" |
| 121 | property :max_rpm, :default => 12500 |
| 122 | property :rpm, :default => 1000 |
| 123 | end |
| 124 | |
| 125 | # This init method will set up your run time environment, |
| 126 | # communication, eventloop, logging etc. We will explain that later. |
| 127 | # |
| 128 | OmfCommon.init(:development, communication: { url: 'xmpp://localhost' }) do |
| 129 | OmfCommon.comm.on_connected do |comm| |
| 130 | info "Garage controoler >> Connected to XMPP server" |
| 131 | garage = OmfRc::ResourceFactory.create(:garage, uid: 'garage') |
| 132 | comm.on_interrupted { garage.disconnect } |
| 133 | end |
| 134 | end |
| 135 | }}} |
| 136 | |
| 137 | Now save the script to a file, then run the script using: |
| 138 | {{{ |
| 139 | ruby <filename> |
| 140 | }}} |
| 141 | |
| 142 | You can verify it is running by compare its output to the following |
| 143 | |
| 144 | ===== Garage controller console output ===== |
| 145 | {{{ |
| 146 | 18:17:46 INFO XMPP::Communicator: Connecting to 'localhost' ... |
| 147 | 18:17:47 INFO Object: Garage controller >> Connected to XMPP server |
| 148 | 18:17:47 DEBUG XMPP::Topic: New topic: garage |
| 149 | 18:17:47 DEBUG XMPP::Communicator: _subscribe >> garage SUCCEED |
| 150 | 18:17:47 DEBUG XMPP::Communicator: publish >> garage SUCCEED |
| 151 | Interact with garage controller |
| 152 | }}} |
| 153 | |
| 154 | ==== Step 2: Request information of the garage ==== |
| 155 | Now since the garage controller is running, we can query its information by sending FRCP request message. We will omf_common library's communicator to achieve this. |
| 156 | |
| 157 | Request information of the garage |
| 158 | |
| 159 | ===== Test script, querying garage ===== |
| 160 | {{{ |
| 161 | # Use omf_common communicator directly |
| 162 | # |
| 163 | require 'omf_common' |
| 164 | |
| 165 | # As seen previously, this init will set up various run time options for you. |
| 166 | # |
| 167 | # First line simply indicates: |
| 168 | # * Use :development as default environment, |
| 169 | # this will use Eventmachine by default, set logging level to :debug |
| 170 | # * Use XMPP as default communication layer and XMPP server to connect to is localhost |
| 171 | # * By default username:password will be auto generated |
| 172 | # |
| 173 | # OmfCommon.comm returns a communicator instance, |
| 174 | # and this will be your entry point to interact with XMPP server. |
| 175 | # |
| 176 | # OmfCommon.eventloop returns Eventmachine runtime instance since it is default. |
| 177 | # |
| 178 | OmfCommon.init(:development, communication: { url: 'xmpp://localhost' }) do |
| 179 | # Event :on_connected will be triggered when connected to XMPP server |
| 180 | # |
| 181 | OmfCommon.comm.on_connected do |comm| |
| 182 | info "Engine test script >> Connected to XMPP" |
| 183 | |
| 184 | # Subscribe to a XMPP topic represents :garage, the name was set in the controller code if you wonder. |
| 185 | # Once triggered, it will yield a Topic object. |
| 186 | # |
| 187 | comm.subscribe('garage') do |garage| |
| 188 | unless garage.error? |
| 189 | # Request two properties from garage, :uid and :type |
| 190 | # |
| 191 | # This is asynchronous, the reply_msg will only get processed when garage received the request |
| 192 | # and we actually received the inform message it issued. |
| 193 | # |
| 194 | # Once we got the reply, simply iterate two properties and print them |
| 195 | # |
| 196 | garage.request([:uid, :type]) do |reply_msg| |
| 197 | reply_msg.each_property do |k, v| |
| 198 | info "#{k} >> #{v}" |
| 199 | end |
| 200 | end |
| 201 | else |
| 202 | error garage.inspect |
| 203 | end |
| 204 | end |
| 205 | |
| 206 | # Eventloop allows to control the flow, in this case, we disconnect after 5 seconds. |
| 207 | # |
| 208 | OmfCommon.eventloop.after(5) { comm.disconnect } |
| 209 | # If you hit ctrl-c, we will disconnect too. |
| 210 | # |
| 211 | comm.on_interrupted { comm.disconnect } |
| 212 | end |
| 213 | end |
| 214 | }}} |
| 215 | |
| 216 | While engine is running, execute this script, you should be able to see garage's properties in the output. |
| 217 | |
| 218 | ===== Console output ===== |
| 219 | {{{ |
| 220 | 13:08:25 INFO XMPP::Communicator: Connecting to 'localhost' ... |
| 221 | 13:08:25 INFO Object: Engine test script >> Connected to XMPP |
| 222 | 13:08:25 DEBUG XMPP::Topic: New topic: garage |
| 223 | 13:08:25 DEBUG XMPP::Communicator: _subscribe >> garage SUCCEED |
| 224 | 13:08:25 DEBUG XMPP::Topic: (garage) create_message_and_publish 'request': [:uid, :type] |
| 225 | 13:08:25 DEBUG XMPP::Communicator: publish >> garage SUCCEED |
| 226 | 13:08:25 INFO Object: uid >> garage |
| 227 | 13:08:25 INFO Object: type >> garage |
| 228 | 13:08:25 INFO Object: hrn >> |
| 229 | 13:08:30 INFO XMPP::Communicator: Disconnecting ... |
| 230 | }}} |
| 231 | |
| 232 | ==== Step 3: Create & release engine ==== |
| 233 | |
| 234 | Now we will let garage to create an engine proxy instance, and once it is created, we will then release it after certain period. This will demonstrate the resource life-cycle described in the FRCP protocol. |
| 235 | |
| 236 | Create & release engine |
| 237 | |
| 238 | ===== Create engine and then release it ===== |
| 239 | |
| 240 | {{{ |
| 241 | require 'omf_common' |
| 242 | |
| 243 | # We define a create_engine method here to contain all the logic around engine creation |
| 244 | # |
| 245 | def create_engine(garage) |
| 246 | # We create an engine instance with a human readable name 'my_engine' |
| 247 | # |
| 248 | garage.create(:engine, hrn: 'my_engine') do |reply_msg| |
| 249 | # This reply_msg will be the inform message issued by garage controller |
| 250 | # |
| 251 | if reply_msg.success? |
| 252 | # Since we need to interact with engine's PubSub topic, |
| 253 | # we call #resource method to construct a topic from the FRCP message content. |
| 254 | # |
| 255 | engine = reply_msg.resource |
| 256 | |
| 257 | # Because of the asynchronous nature, we need to use this on_subscribed callback |
| 258 | # to make sure the operation in the block executed only when subscribed to the newly created engine's topic |
| 259 | engine.on_subscribed do |
| 260 | info ">>> Connected to newly created engine #{reply_msg[:hrn]}(id: #{reply_msg[:res_id]})" |
| 261 | end |
| 262 | |
| 263 | # Then later on, we will ask garage again to release this engine. |
| 264 | # |
| 265 | OmfCommon.eventloop.after(3) do |
| 266 | release_engine(garage, engine) |
| 267 | end |
| 268 | else |
| 269 | error ">>> Resource creation failed - #{reply_msg[:reason]}" |
| 270 | end |
| 271 | end |
| 272 | end |
| 273 | |
| 274 | def release_engine(garage, engine) |
| 275 | info ">>> Release engine" |
| 276 | # Only parent (garage) can release its child (engine) |
| 277 | # |
| 278 | garage.release(engine) do |reply_msg| |
| 279 | info "Engine #{reply_msg[:res_id]} released" |
| 280 | OmfCommon.comm.disconnect |
| 281 | end |
| 282 | end |
| 283 | |
| 284 | OmfCommon.init(:development, communication: { url: 'xmpp://localhost' }) do |
| 285 | OmfCommon.comm.on_connected do |comm| |
| 286 | info "Engine test script >> Connected to XMPP" |
| 287 | |
| 288 | comm.subscribe('garage') do |garage| |
| 289 | unless garage.error? |
| 290 | # Now calling create_engine method we defined, with newly created garage topic object |
| 291 | # |
| 292 | create_engine(garage) |
| 293 | else |
| 294 | error garage.inspect |
| 295 | end |
| 296 | end |
| 297 | |
| 298 | OmfCommon.eventloop.after(10) { comm.disconnect } |
| 299 | comm.on_interrupted { comm.disconnect } |
| 300 | end |
| 301 | end |
| 302 | }}} |
| 303 | |
| 304 | While garage controller is running, execute this script and check the output |
| 305 | |
| 306 | Console output |
| 307 | {{{ |
| 308 | 13:20:50 INFO XMPP::Communicator: Connecting to 'localhost' ... |
| 309 | 13:20:50 INFO Object: Engine test script >> Connected to XMPP |
| 310 | 13:20:50 DEBUG XMPP::Topic: New topic: garage |
| 311 | 13:20:50 DEBUG XMPP::Communicator: _subscribe >> garage SUCCEED |
| 312 | 13:20:50 DEBUG XMPP::Topic: Create resource of type 'engine' |
| 313 | 13:20:50 DEBUG XMPP::Topic: (garage) create_message_and_publish 'create': {:hrn=>"my_engine", :type=>:engine} |
| 314 | 13:20:50 DEBUG XMPP::Communicator: publish >> garage SUCCEED |
| 315 | 13:20:50 DEBUG XMPP::Topic: New topic: xmpp://1e1c5fd6-6d7a-4375-a48c-9dddbdf05394@interlagos |
| 316 | 13:20:50 DEBUG XMPP::Communicator: _subscribe >> 1e1c5fd6-6d7a-4375-a48c-9dddbdf05394 SUCCEED |
| 317 | 13:20:50 INFO Object: >>> Connected to newly created engine my_engine(id: xmpp://1e1c5fd6-6d7a-4375-a48c-9dddbdf05394@interlagos) |
| 318 | 13:20:53 INFO Object: >>> Release engine |
| 319 | 13:20:53 DEBUG XMPP::Communicator: publish >> garage SUCCEED |
| 320 | 13:20:59 INFO Object: Engine xmpp://1e1c5fd6-6d7a-4375-a48c-9dddbdf05394@interlagos released |
| 321 | 13:20:59 INFO XMPP::Communicator: Disconnecting ... |
| 322 | }}} |
| 323 | |
| 324 | ==== Step 4: Add more features to garage engine proxies ==== |
| 325 | |
| 326 | Now we are going to add a feature of controlling throttle to the engine proxy, so that we can apply different level of throttle and monitor engine's RPM value. |
| 327 | |
| 328 | Configure engine property |
| 329 | |
| 330 | To allow this, we need to update our garage controller to allow configuring throttle, and provide some simple RPM calculation. |
| 331 | |
| 332 | Modified garage proxy |
| 333 | {{{ |
| 334 | require 'omf_rc' |
| 335 | |
| 336 | module OmfRc::ResourceProxy::Garage |
| 337 | include OmfRc::ResourceProxyDSL |
| 338 | |
| 339 | register_proxy :garage |
| 340 | end |
| 341 | |
| 342 | module OmfRc::ResourceProxy::Engine |
| 343 | include OmfRc::ResourceProxyDSL |
| 344 | |
| 345 | register_proxy :engine, :create_by => :garage |
| 346 | |
| 347 | property :manufacturer, :default => "Cosworth" |
| 348 | property :max_rpm, :default => 12500 |
| 349 | # Add additional property to store rpm and throttle |
| 350 | # |
| 351 | property :rpm, :default => 1000 |
| 352 | property :throttle, :default => 0 |
| 353 | |
| 354 | hook :before_ready do |engine| |
| 355 | # Constantly calculate RPM value, rules are: |
| 356 | # |
| 357 | # * Applying 100% throttle will increase RPM by 5000 per second |
| 358 | # * Engine will reduce RPM by 500 per second when no throttle applied |
| 359 | # |
| 360 | OmfCommon.eventloop.every(2) do |
| 361 | engine.property.rpm += (engine.property.throttle * 5000 - 500).to_i |
| 362 | engine.property.rpm = 1000 if engine.property.rpm < 1000 |
| 363 | end |
| 364 | end |
| 365 | |
| 366 | # Then we simply register a configure property handler for throttle, |
| 367 | # We expect a percentage value received and convert into decimal value |
| 368 | # |
| 369 | configure :throttle do |engine, value| |
| 370 | engine.property.throttle = value.to_f / 100.0 |
| 371 | end |
| 372 | end |
| 373 | |
| 374 | OmfCommon.init(:development, communication: { url: 'xmpp://localhost' }) do |
| 375 | OmfCommon.comm.on_connected do |comm| |
| 376 | info "Garage controoler >> Connected to XMPP server" |
| 377 | garage = OmfRc::ResourceFactory.create(:garage, uid: 'garage') |
| 378 | comm.on_interrupted { garage.disconnect } |
| 379 | end |
| 380 | end |
| 381 | }}} |
| 382 | |
| 383 | We then modify the engine test script to configure throttle, and request RPM values. |
| 384 | |
| 385 | ===== Engine test ===== |
| 386 | |
| 387 | {{{ |
| 388 | require 'omf_common' |
| 389 | |
| 390 | def create_engine(garage) |
| 391 | garage.create(:engine, hrn: 'my_engine') do |reply_msg| |
| 392 | if reply_msg.success? |
| 393 | engine = reply_msg.resource |
| 394 | |
| 395 | engine.on_subscribed do |
| 396 | info ">>> Connected to newly created engine #{reply_msg[:hrn]}(id: #{reply_msg[:res_id]})" |
| 397 | on_engine_created(engine) |
| 398 | end |
| 399 | |
| 400 | OmfCommon.eventloop.after(10) do |
| 401 | release_engine(garage, engine) |
| 402 | end |
| 403 | else |
| 404 | error ">>> Resource creation failed - #{reply_msg[:reason]}" |
| 405 | end |
| 406 | end |
| 407 | end |
| 408 | |
| 409 | def on_engine_created(engine) |
| 410 | info "> Now we will apply 50% throttle to the engine" |
| 411 | engine.configure(throttle: 50) |
| 412 | |
| 413 | # Every 2 seconds, we send a request to engine, request its RPM value |
| 414 | # |
| 415 | OmfCommon.eventloop.every(2) do |
| 416 | engine.request([:rpm]) do |reply_msg| |
| 417 | info "RPM >> #{reply_msg[:rpm]}" |
| 418 | end |
| 419 | end |
| 420 | |
| 421 | # Some time later, we configure the throttle back to 0 |
| 422 | # |
| 423 | OmfCommon.eventloop.after(5) do |
| 424 | info "> We want to reduce the throttle to 0" |
| 425 | engine.configure(throttle: 0) |
| 426 | end |
| 427 | end |
| 428 | |
| 429 | def release_engine(garage, engine) |
| 430 | info ">>> Release engine" |
| 431 | garage.release(engine) do |reply_msg| |
| 432 | info "Engine #{reply_msg[:res_id]} released" |
| 433 | OmfCommon.comm.disconnect |
| 434 | end |
| 435 | end |
| 436 | |
| 437 | OmfCommon.init(:development, communication: { url: 'xmpp://localhost' }) do |
| 438 | OmfCommon.comm.on_connected do |comm| |
| 439 | info "Engine test script >> Connected to XMPP" |
| 440 | |
| 441 | comm.subscribe('garage') do |garage| |
| 442 | unless garage.error? |
| 443 | create_engine(garage) |
| 444 | else |
| 445 | error garage.inspect |
| 446 | end |
| 447 | end |
| 448 | |
| 449 | OmfCommon.eventloop.after(20) { comm.disconnect } |
| 450 | comm.on_interrupted { comm.disconnect } |
| 451 | end |
| 452 | end |
| 453 | }}} |
| 454 | |
| 455 | Now in the output you could notice that RPM value increased and then decreased. |
| 456 | |
| 457 | ===== Engine test output ===== |
| 458 | {{{ |
| 459 | 15:52:32 INFO XMPP::Communicator: Connecting to 'localhost' ... |
| 460 | 15:52:32 INFO Object: Engine test script >> Connected to XMPP |
| 461 | 15:52:32 DEBUG XMPP::Topic: New topic: garage |
| 462 | 15:52:32 DEBUG XMPP::Communicator: _subscribe >> garage SUCCEED |
| 463 | 15:52:32 DEBUG XMPP::Topic: Create resource of type 'engine' |
| 464 | 15:52:32 DEBUG XMPP::Topic: (garage) create_message_and_publish 'create': {:hrn=>"my_engine", :type=>:engine} |
| 465 | 15:52:32 DEBUG XMPP::Communicator: publish >> garage SUCCEED |
| 466 | 15:52:32 DEBUG XMPP::Topic: New topic: xmpp://f9288cef-ae76-4561-a199-276800efe029@interlagos |
| 467 | 15:52:32 DEBUG XMPP::Communicator: _subscribe >> f9288cef-ae76-4561-a199-276800efe029 SUCCEED |
| 468 | 15:52:32 INFO Object: >>> Connected to newly created engine my_engine(id: xmpp://f9288cef-ae76-4561-a199-276800efe029@interlagos) |
| 469 | 15:52:32 INFO Object: > Now we will apply 50% throttle to the engine |
| 470 | 15:52:32 DEBUG XMPP::Topic: (f9288cef-ae76-4561-a199-276800efe029) create_message_and_publish 'configure': {:throttle=>50} |
| 471 | 15:52:32 DEBUG XMPP::Communicator: publish >> f9288cef-ae76-4561-a199-276800efe029 SUCCEED |
| 472 | 15:52:34 DEBUG XMPP::Topic: (f9288cef-ae76-4561-a199-276800efe029) create_message_and_publish 'request': [:rpm] |
| 473 | 15:52:34 DEBUG XMPP::Communicator: publish >> f9288cef-ae76-4561-a199-276800efe029 SUCCEED |
| 474 | 15:52:34 INFO Object: RPM >> 3000 |
| 475 | 15:52:36 DEBUG XMPP::Topic: (f9288cef-ae76-4561-a199-276800efe029) create_message_and_publish 'request': [:rpm] |
| 476 | 15:52:36 DEBUG XMPP::Communicator: publish >> f9288cef-ae76-4561-a199-276800efe029 SUCCEED |
| 477 | 15:52:36 INFO Object: RPM >> 5000 |
| 478 | 15:52:37 INFO Object: > We want to reduce the throttle to 0 |
| 479 | 15:52:37 DEBUG XMPP::Topic: (f9288cef-ae76-4561-a199-276800efe029) create_message_and_publish 'configure': {:throttle=>0} |
| 480 | 15:52:37 DEBUG XMPP::Communicator: publish >> f9288cef-ae76-4561-a199-276800efe029 SUCCEED |
| 481 | 15:52:38 DEBUG XMPP::Topic: (f9288cef-ae76-4561-a199-276800efe029) create_message_and_publish 'request': [:rpm] |
| 482 | 15:52:38 DEBUG XMPP::Communicator: publish >> f9288cef-ae76-4561-a199-276800efe029 SUCCEED |
| 483 | 15:52:38 INFO Object: RPM >> 4500 |
| 484 | 15:52:40 DEBUG XMPP::Topic: (f9288cef-ae76-4561-a199-276800efe029) create_message_and_publish 'request': [:rpm] |
| 485 | 15:52:40 DEBUG XMPP::Communicator: publish >> f9288cef-ae76-4561-a199-276800efe029 SUCCEED |
| 486 | 15:52:40 INFO Object: RPM >> 4000 |
| 487 | 15:52:42 INFO Object: >>> Release engine |
| 488 | 15:52:42 DEBUG XMPP::Communicator: publish >> garage SUCCEED |
| 489 | 15:52:42 DEBUG XMPP::Topic: (f9288cef-ae76-4561-a199-276800efe029) create_message_and_publish 'request': [:rpm] |
| 490 | 15:52:42 DEBUG XMPP::Communicator: publish >> f9288cef-ae76-4561-a199-276800efe029 SUCCEED |
| 491 | 15:52:42 INFO Object: RPM >> 3500 |
| 492 | 15:52:44 DEBUG XMPP::Topic: (f9288cef-ae76-4561-a199-276800efe029) create_message_and_publish 'request': [:rpm] |
| 493 | 15:52:44 DEBUG XMPP::Communicator: publish >> f9288cef-ae76-4561-a199-276800efe029 SUCCEED |
| 494 | 15:52:44 INFO Object: RPM >> 3000 |
| 495 | 15:52:46 DEBUG XMPP::Topic: (f9288cef-ae76-4561-a199-276800efe029) create_message_and_publish 'request': [:rpm] |
| 496 | 15:52:46 DEBUG XMPP::Communicator: publish >> f9288cef-ae76-4561-a199-276800efe029 SUCCEED |
| 497 | 15:52:46 INFO Object: RPM >> 2500 |
| 498 | 15:52:47 INFO Object: Engine xmpp://f9288cef-ae76-4561-a199-276800efe029@interlagos released |
| 499 | 15:52:47 INFO XMPP::Communicator: Disconnecting ... |
| 500 | }}} |
| 501 | |
| 502 | ==== Step 5: Hooks ==== |
| 503 | |
| 504 | OMF allow you to define hook callbacks, which basically can be called at certain stage of the operation. |
| 505 | |
| 506 | Currently the system supports these hooks: |
| 507 | |
| 508 | Execute in parent resource |
| 509 | |
| 510 | before_create, called before parent creates the child resource. (in the context of parent resource) |
| 511 | after_create, called after parent creates the child resource. |
| 512 | Execute in child resource |
| 513 | |
| 514 | before_ready, called when a resource created, before creating an associated pubsub topic |
| 515 | before_release, called before a resource released |
| 516 | after_initial_configured, called after child resource created, and initial set of properties have been configured. |
| 517 | Please also refer to DSL hook method for more information. |
| 518 | |
| 519 | Hooks |
| 520 | |
| 521 | Now modify the garage controller to test these hooks. |
| 522 | |
| 523 | Proxy with hooks |
| 524 | |
| 525 | require 'omf_rc' |
| 526 | |
| 527 | module OmfRc::ResourceProxy::Garage |
| 528 | include OmfRc::ResourceProxyDSL |
| 529 | |
| 530 | register_proxy :garage |
| 531 | |
| 532 | # before_create allows you access the current garage instance, the type of new resource it is going to create, |
| 533 | # and initial options passed to be used for new resource |
| 534 | # |
| 535 | hook :before_create do |garage, new_resource_type, new_resource_opts| |
| 536 | # Can check existing engines already created |
| 537 | # |
| 538 | info "Garage has #{garage.children.size} engine(s)" |
| 539 | |
| 540 | # Can verify new resource's options |
| 541 | # |
| 542 | info "You asked me to create a new #{new_resource_type} with options: #{new_resource_opts}" |
| 543 | end |
| 544 | |
| 545 | # after_create hook has access to the current garage instance and newly created engine instance |
| 546 | # |
| 547 | hook :after_create do |garage, engine| |
| 548 | # Can inspect or update newly created resource |
| 549 | # |
| 550 | info "Engine #{engine.uid} created" |
| 551 | end |
| 552 | end |
| 553 | |
| 554 | module OmfRc::ResourceProxy::Engine |
| 555 | include OmfRc::ResourceProxyDSL |
| 556 | |
| 557 | register_proxy :engine, :create_by => :garage |
| 558 | |
| 559 | property :serial_number, :default => "0000" |
| 560 | property :rpm, :default => 0 |
| 561 | |
| 562 | # Use this to do initialisation/bootstrap |
| 563 | # |
| 564 | hook :before_ready do |engine| |
| 565 | engine.property.rpm = 1000 |
| 566 | # Notice that now serial number hasn't been configured yet. |
| 567 | # |
| 568 | info "Engine serial number is #{engine.property.serial_number}" |
| 569 | end |
| 570 | |
| 571 | # Since now new resource has been created and configured properly, |
| 572 | # additional logic can be applied based on configured properties' state. |
| 573 | # |
| 574 | hook :after_initial_configured do |engine| |
| 575 | # Notice now serial number is configured. |
| 576 | # |
| 577 | info "Engine serial number is #{engine.property.serial_number}" |
| 578 | end |
| 579 | |
| 580 | # before_release hook will be called before the resource is fully released, shut down the engine in this case. |
| 581 | # |
| 582 | hook :before_release do |engine| |
| 583 | engine.property.rpm = 0 |
| 584 | end |
| 585 | end |
| 586 | |
| 587 | OmfCommon.init(:development, communication: { url: 'xmpp://localhost' }) do |
| 588 | OmfCommon.comm.on_connected do |comm| |
| 589 | info "Garage controoler >> Connected to XMPP server" |
| 590 | garage = OmfRc::ResourceFactory.create(:garage, uid: 'garage', hrn: 'my_garage') |
| 591 | comm.on_interrupted { garage.disconnect } |
| 592 | end |
| 593 | end |
| 594 | Then use this simple engine test script to run through the resource cycle, from create to release. |
| 595 | |
| 596 | Engine test |
| 597 | {{{ |
| 598 | require 'omf_common' |
| 599 | |
| 600 | def create_engine(garage) |
| 601 | garage.create(:engine, hrn: 'my_engine', serial_number: '1234') do |reply_msg| |
| 602 | if reply_msg.success? |
| 603 | engine = reply_msg.resource |
| 604 | |
| 605 | engine.on_subscribed do |
| 606 | info ">>> Connected to newly created engine #{reply_msg[:hrn]}(id: #{reply_msg[:res_id]})" |
| 607 | end |
| 608 | |
| 609 | OmfCommon.eventloop.after(3) do |
| 610 | release_engine(garage, engine) |
| 611 | end |
| 612 | else |
| 613 | error ">>> Resource creation failed - #{reply_msg[:reason]}" |
| 614 | end |
| 615 | end |
| 616 | end |
| 617 | |
| 618 | def release_engine(garage, engine) |
| 619 | info ">>> Release engine" |
| 620 | garage.release(engine) do |reply_msg| |
| 621 | info "Engine #{reply_msg[:res_id]} released" |
| 622 | OmfCommon.comm.disconnect |
| 623 | end |
| 624 | end |
| 625 | |
| 626 | OmfCommon.init(:development, communication: { url: 'xmpp://localhost' }) do |
| 627 | OmfCommon.comm.on_connected do |comm| |
| 628 | info "Engine test script >> Connected to XMPP" |
| 629 | |
| 630 | comm.subscribe('garage') do |garage| |
| 631 | unless garage.error? |
| 632 | create_engine(garage) |
| 633 | else |
| 634 | error garage.inspect |
| 635 | end |
| 636 | end |
| 637 | |
| 638 | OmfCommon.eventloop.after(10) { comm.disconnect } |
| 639 | comm.on_interrupted { comm.disconnect } |
| 640 | end |
| 641 | end |
| 642 | }}} |
| 643 | |
| 644 | While you are running garage controller and test script, you should be able to see the following log message on the garage controller side. |
| 645 | |
| 646 | Please check the 'info' messages and you could notice the order of how these hooks got executed. Verify the order against the diagram we showed earlier. |
| 647 | |
| 648 | Proxy output |
| 649 | |
| 650 | {{{ |
| 651 | 15:33:03 INFO XMPP::Communicator: Connecting to 'localhost' ... |
| 652 | 15:33:03 INFO Object: Garage controoler >> Connected to XMPP server |
| 653 | 15:33:03 DEBUG XMPP::Topic: New topic: garage |
| 654 | 15:33:03 DEBUG XMPP::Communicator: _subscribe >> garage SUCCEED |
| 655 | 15:33:03 DEBUG XMPP::Communicator: publish >> garage SUCCEED |
| 656 | 15:33:12 INFO ResourceProxy::Garage: Garage has 0 engine(s) |
| 657 | 15:33:12 INFO ResourceProxy::Garage: You asked me to create a new engine with options: {:hrn=>"my_engine"} |
| 658 | 15:33:12 DEBUG XMPP::Topic: New topic: 9ae92796-bcd7-4770-a698-78df44d63fe2 |
| 659 | 15:33:12 INFO ResourceProxy::Engine: Engine serial number is 0000 |
| 660 | 15:33:12 INFO ResourceProxy::Garage: Engine 9ae92796-bcd7-4770-a698-78df44d63fe2 created |
| 661 | 15:33:12 DEBUG XMPP::Communicator: _create >> 9ae92796-bcd7-4770-a698-78df44d63fe2 SUCCEED |
| 662 | 15:33:12 DEBUG XMPP::Communicator: _subscribe >> 9ae92796-bcd7-4770-a698-78df44d63fe2 SUCCEED |
| 663 | 15:33:12 INFO ResourceProxy::Engine: Engine serial number is 1234 |
| 664 | 15:33:12 DEBUG XMPP::Communicator: publish >> garage SUCCEED |
| 665 | 15:33:12 DEBUG XMPP::Communicator: publish >> 9ae92796-bcd7-4770-a698-78df44d63fe2 SUCCEED |
| 666 | 15:33:15 INFO ResourceProxy::AbstractResource: Releasing hrn: my_engine, uid: 9ae92796-bcd7-4770-a698-78df44d63fe2 |
| 667 | }}} |
| 668 | |
| 669 | That concludes our little tutorial for you. |
| 670 | |
| 671 | ==== Proxies included in the official RC gem ==== |
| 672 | |
| 673 | OMF RC has included some resource proxies to support network configuration and running OMF enabled applications. For more details, refer to OmfRc::ResourceProxy::AbstractResource. |
| 674 | |
| 675 | Application Proxy |
| 676 | Application proxy has been provided to set up and control applications. We include a separate guide explaining how it works. See How to use the Application Proxy. It is also a good example demonstrating how to write a non-trivial resource proxy. |
| 677 | |
| 678 | Logging |
| 679 | Given controlling logging is important for developing and debugging, we created a dedicated guide. |
| 680 | |
| 681 | Logging system in OMF 6 |
| 682 | |
| 683 | Advanced topics |
| 684 | Organise resource proxy modules |
| 685 | Define inline |
| 686 | |
| 687 | If you have a rather simple resource controller, with minimal set of features, like the ones described in this tutorial, you could just define these modules as part of the RC script. |
| 688 | |
| 689 | Include resource proxy modules in the default package |
| 690 | |
| 691 | The default location of resource proxy definition files are located in the directory omf_rc/lib/omf_rc/resource_proxy. |
| 692 | |
| 693 | If you wish your feature set could be available as part of the default package, save them under this default directory, following this naming convention: OmfRc::ResourceProxy::Engine will register a proxy named :engine, and saved to file omf_rc/lib/omf_rc/resource_proxy/engine.rb |
| 694 | |
| 695 | To load these default resource proxies, simple call a load method provided by ResourceFactory class in your resource controller script (e.g. engine_control.rb) |
| 696 | |
| 697 | OmfRc::ResourceFactory.load_default_resource_proxies |
| 698 | Commit your definition files into the git repository and simply send us a pull request. |
| 699 | |
| 700 | Package your proxy definition files as OMF extension gem |
| 701 | |
| 702 | You could also package your proxy definition files into separate gems, if you feel they should not go into the default RC package. |
| 703 | |
| 704 | This process is rather simple, take a look at this third party rc gem of openflow integration. |
| 705 | |
| 706 | https://github.com/kohoumas/omf_rc_openflow |
| 707 | |
| 708 | Refactor common features into resource utilities |
| 709 | If a set of features can be shared among different types of resources, it is a good idea to refactor them into resource utilities. |
| 710 | |
| 711 | Take this engine test example, if we have more than one type of engine needs to be tested, and they could all be able to adjust throttle, we can create a utility for this. |
| 712 | {{{ |
| 713 | module OmfRc::Util::Throttle |
| 714 | include OmfRc::ResourceProxyDSL |
| 715 | |
| 716 | configure :throttle do |resource, value| |
| 717 | resource.property.throttle = value.to_f / 100.0 |
| 718 | end |
| 719 | end |
| 720 | }}} |
| 721 | Then include this utility inside the engine resource proxy file by using: |
| 722 | {{{ |
| 723 | utility :throttle |
| 724 | }}} |
| 725 | |
| 726 | You could also overwrite a property definition provided by the utility, by registering it again using the same name. |
| 727 | |
| 728 | === Contributing to OMF === |
| 729 | Ready to be part of OMF project? Please refer to [wiki:OMF/OMF60/1Developer/1Contributing Contributing] document for some guidelines. |