マイクロソフト株式会社 デベロッパー&プラットフォーム統括本部 プラットフォームストラテジスト 佐藤 直樹 はじめに Windows Azure ストレージとは Windows Azure ストレージ詳細 ブロブ、テーブル、キュー まとめ Windows Azure とは Windows Azure はマイクロソフトのクラ ウドプラットフォームの基盤技術 クラウドのための OS クラウドのための主要なサービスを提供 仮想マシン環境 拡張性のあるストレージ 管理の自動化 開発者向け SDK Windows Azure に含まれるストレージサー ビス 堅牢性、拡張性、可用性 セキュリティ、パフォーマンス データーの抽象化 ブロブ、テーブル、キュー シンプルで使いなれたプログラミングイン ターフェイス REST と ADO.NET アカウント ユニークなストレージアカウント 例: “naokisstorage” 256 bit シークレット キー セキュリティ シークレットキーを使用しリクエストに対す る HMAC-SHA256 アルゴリズムでの署名 署名を使用しサーバへの認証リクエスト アカウント http://<アカウント>.<種類>.core.windows.net/ ブロブ テーブル キュー 3種類のストレージ ブロブ 単純な階層を持つファイルストレージ テーブル リレーショナルを持たない構造化ストレージ キュー メッセージ交換のための信頼性のあるストレー ジ ブロブ テーブル キュー 3種類のストレージの主な用途 ストレージ 主な用途 ブロブ データ保存に利用。バイナリデータの集合を保存する、もっとも 基本的なストア方法。大きなブロブは複数のブロックに分割する ことが可能。ブロック単位で再送し、データ転送時のエラーへ対 処可能。 テーブル データ保存に利用。エンティティーと型情報を持つプロパティを 組み合わせた単純な階層として保持。ブロブより粒度の細かい データを取り扱う時利用。リレーショナル構造を持たない。 キュー メッセージ交換に利用。Web ロールインスタンスと Worker ロー ルインスタンスとのデータ通信に利用。大きなサイズのデータを 交換する場合、ブロブやテーブルにデータを保持し、保持先のみ をメッセージとして通信。 ブロブ テーブル キュー デベロップメントストレージサービス クラウドストレージのエミュレーション オフライン環境、ローカル環境で開発・テ ストのために提供 SQL Server Express 2005/2008 が要件 URIの違いに注意 http://<local-machine-address>:<port>/<accountname>/<resource-path> Windows Azure ストレージサービス ①ストレージアカウ ントの作成 ②エンドポイントの 確認 ②アクセスキーの確 認(再生成) 3種類のストレージ ブロブ 単純な階層を持つファイル ストレージ テーブル リレーショナルを持たない構造化ストレージ キュー メッセージのための信頼性のあるストレージ ブロブ テーブル キュー 単純な階層を持つファイルストレージ バイナリデータの集合 コンテナ アカウントには1つもしくは複数のコンテナを保持 コンテナは複数のブロブの集合 コンテナレベルでポリシーを共有 メタデータ (<name, value>, 8KBまで) ブロブ キャパシティー 最大50GB ブロック サイズが大きいブロブを 複数のブロックにより構築 REST でのアクセス http://<アカウント>.blob.core.windows.net/<コンテナ> アカウント、コンテナ、ブロブ アカウント コンテナ ブロブ IMG001.JPG pictures IMG002.JPG sato movies MOV1.AVI http://<アカウント>.blob.core.windows.net/<コンテナ> アカウント、コンテナ、ブロブ、ブロック アカウント コンテナ ブロブ IMG001. JPG ブロック pictures IMG002. JPG sato Block 1 movies MOV1.AVI Block 2 Block 3 http://<アカウント>.blob.core.windows.net/<コンテナ> ブロブ コンテナ Create/Delete Metadata, ACL の設定 ブロブの列挙 ブロブ Get/Put/Delete Metadata の設定 ブロックリストの取得、設定 ブロック分割 大きいブロブのアップロード リトライ処理、並行実行 Block Id N Block Id 3 Block Id 1 Block Id 2 10 GB Movie ※ 動作の概念を説明しています。 StrageClient 内 PutBlobImpl を参考 blobName = “TheBlob.wmv”; PutBlock(blobName, blockId1, block1Bits); PutBlock(blobName, blockId2, block2Bits); ………… PutBlock(blobName, blockIdN, blockNBits); PutBlockList(blobName, blockId1,…,blockIdN); コミット最大50,000 TheBlob.wmv TheBlob.wmv Windows Azure ストレージ アップロード Block Id 2 Block Id 4 Block Id 3 Block Id 4 Block Id 2 Block Id 3 Block Id 4 Block Id 1 BlobName = ExampleBlob.wmv Out-of-Order(順序入替) 同一 Block ID 使わないブロック [シーケンス] 同一 Block IDの 場合、最後の アップロードが 優先される Block IDコレク ションにより、 ブロブが生成さ れる PutBlock(BlockId1) PutBlock(BlockId3) PutBlock(BlockId4) PutBlock(BlockId2) PutBlock(BlockId4) PutBlockList(BlockId2, BlockId3, BlockId4) REST (Put/Get/Delete) のインターフェイス オフセットや長さによる読み込み可能 ブロブ最大サイズ 50 GB : PutBlock と PutBlockList 64 MB : PutBlob ブロックはブロブのアップロード中断後の再 開を実現 CTPでは Put Blob/BlockList = Blob置換 既存のブロブを新しいブロブ・ブロックで置換可 能 今後の予定: Update/Append/Copy Blob 3種類のストレージ ブロブ 単純な階層を持つファイル ストレージ テーブル リレーショナルを持たない構造化ストレージ キュー メッセージのための信頼性のあるストレージ ブロブ テーブル キュー 構造化ストレージ TBクラスのデータの取り扱い スケール可能(拡張性)なテーブル 非リレーショナルDB テーブル エンティティーとプロパティの集合 ロー(Rows) :エンティティーの集合 カラム(Columns) : プロパティの集合 プログラミング ADO.NET Data Services .NET アクセス LINQ REST でのアクセス http://<アカウント>.table.core.windows.net/<テーブル名> アカウント、テーブル、エンティティ アカウント テーブル エンティ ティ Name=…has h=… users Name=…has h=… sato photoindex Tag=…id= … http://<アカウント>.table.core.windows.net/<テーブル名> テーブル テーブル Create/Delete Query エンティティ Insert/Update/Merge/Delete Query 255個までのプロパティを持つ プロパティ <Name, TypedValue> ペアが保存 必須 Property 3 Modification Time ….. V1.0 3/21/2007 ….. 福利厚生Doc V1.0.6 9/28/2007 2008年度用 山田作成中 勤怠Doc V1.0 3/28/2007 2007年度 勤怠Doc V1.0.1 7/6/2007 2008年度用 千田作成中 Partition Key Document Name Row Key Version 福利厚生Doc Property N Description 異なるプロパティ を 持ってもよい 2007年度 スキーマを持たない Partition Key/Row Key String (64KBまで) その他のプロパティ String (64KBまで) Binary (64KBまで) Bool DateTime GUID Int Int64 Double Partition Key を用いたクエリ Partition Key Document Name Row Key Version Property 3 Modification Time ….. Property N Description 福利厚生Doc V1.0 3/21/2007 ….. 2007年度 福利厚生Doc V1.0.6 9/28/2007 Partition 2008年度用 山田作成中 1 勤怠Doc V1.0 3/28/2007 2007年度 勤怠Doc V1.0.1 7/6/2007 Partition 2008年度用 千田作成中 2 勤怠Doc V1.0.3 9/11/2007 2008年度用 鈴木作成中 “勤怠Doc” を取り出すと き、単一パーティション のため効率的 2つのパーティションは スケールアウトのため異 なるサーバに保存 単一パーティションへの PartitionKey ==“勤怠Doc” はパフォーマンス良 複数パーティションへの ModifiedTime > 7/01/2007 はコスト高 パーティションとパーティションキー テーブル パーティション テーブル内のエンティティのグルーピングとして利 用 Partition Key (パーティションキー) パーティションキーにより、アプリケーションから パーティションの粒度を制御 エンティティ 2つのキーによりテーブル内で一意 Partition Key : 配置されるパーティション Row Key : パーティション内でエンティティを一意 パフォーマンス&スケーラビリティー パフォーマンス パーティションによりエンティティの保存場所が 決定される 同一のパーティションキーを持つエンティティは、 同一のパーティションに保存される 効率的なクエリーとキャッシュ スケーラビリティ パーティションに対するトラフィックを監視 自動的にパーティションのロードバランス パーティション毎に違ったストレージノードに保存され る可能性を持つ パーティションを活用することで、容易なロード バランス Message を作成し、テーブルに挿入 Message message = new Message { PartitionKey = “Japan", // ChannelName RowKey = DateTime.UtcNow.ToString(), // PostedDate Text = "Hello Azure", Rating = 3 }; serviceUri = new Uri("http://<account>.table.core.windows.net"); var context = new DataServiceContext(serviceUri); context.AddObject("Messages", message); DataServiceContext response = context.SaveChanges(); Atom XML ペイロードをポスト POST http://<Account>.table.core.windows.net/Messages ... <!– Atom envelope --> <m:properties> <d:PartitionKey>Japan</d:PartitionKey> <!-- ChannelName --> <d:RowKey>Feb-19</d:RowKey> <!-- PostedDate --> <d:Text>Hello Azure</d:Text> <d:Rating>3</d:Rating> </m:properties> 大規模な拡張性、可用性、耐久性を持つ構造化ス トレージを提供 自動ロードバランスとテーブル拡張 パーティションキーをエクスポーズ REST や LINQ によるインターフェイス ADO.NET Data Services “リレーショナルDB” ではない JOINなし、外部キーなし、etc… 今後の予定: セカンダリインデックス、エンティティグループ 3種類のストレージ ブロブ 単純な階層を持つファイル ストレージ テーブル リレーショナルを持たない構造化ストレージ キュー メッセージのための信頼性のあるストレージ ブロブ テーブル キュー リライアブルなメッセージ配信 シンプル、非同期なキュー キュー メッセージは少なくとも1回検索 メッセージ メッセージ数上限なし メッセージ最大サイズ 8KB 高可用性、耐久性、パフォーマンス REST でのアクセス http://<アカウント>.queue.core.windows.net/<キュー名> アカウント、キュー、メッセージ アカウント キュー メッセージ http://... uploadjob http://... sato indexjob http://... http://<アカウント>.queue.core.windows.net/<キュー名> キュー キュー Create/Clear/Delete キューの長さ メッセージ Put/Get Peek/Delete/Clear タイムアウト付での読み出し Producers Consumers C1 P2 4 P1 3 2 1. Dequeue(Q, 30 sec) msg 1 1 C2 2. Dequeue(Q, 30 sec) msg 2 タイムアウト付での読み出し Producers Consumers 1 P2 4 33 2 1 2 P1 C1 C2 1. Dequeue(Q, 30 sec) msg 1 5. C1 クラッシュ 6. タイムアウト指定により、 msg1 は Dequeue 後(30秒 後)読出し可能 2. Dequeue(Q, 30 sec) msg 2 3. C2 により msg 2 が処理 4. Delete(Q, msg 2) 7. Dequeue(Q, 30 sec) msg 1 タイムアウトの 規定値 30秒、 最大2時間。 可用性、拡張性 デベロップメントストレージサービス SQL Data Services テーブル 同時更新 ページ付け テーブル一貫性 高耐久性 ~ 自己監視&自己修復 データーは少なくとも3か所のノードにレ プリカを保持 レプリカは同じデータセンターの異なったドメ イン上に展開 すべてのストレージはレプリケーションレ イヤーに構築 動的レプリケーションにより適切なレプリカ数 を維持 ロスト/未応答のドライブやノードから回復 ビット欠落データーからの回復 ビット欠落に備えデータへの連続走査 可用性と拡張性 効率的なフェイルオーバー データセンター内で利用可能なレプリカを用い てサービス実行 頻度の高いデータの自動ロードバランス 利用パターンを監視し、ブロブやテーブル、 キューへのアクセスをロードバランス データセンター上の頻度の高いデータへのトラ フィックに従ってアクセスを分散 ブロブ、エンティティ、キューのキャッシ ング ブロブはスケールアウトアクセス エンティティとキューはメモリ デベロップメントストレージサービス 開発SDK サンプル利用時の App.config StorageClient サンプル アカウント名、キー値は固定 <configuration> <appSettings> <add key = "AccountName" value="devstoreaccount1"/> <add key = "AccountSharedKey" value="Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2U VErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="/> <add key="BlobStorageEndpoint" value="http://127.0.0.1:10000"/> <add key="QueueStorageEndpoint" value="http://127.0.0.1:10001"/> <add key=“TableStorageEndpoint” value=“http://127.0.0.1:10002”/> … </appSettings> <configuration> SQL Data Servieces Windows Azure スト レージ クラウド環境のエッセン シャルストレージ 非リレーショナルデータ を取り扱う SQL Data Services クラウド環境のデータ ベースサービス SQL プラットフォームの クラウド環境への拡張 リレーショナルデータ処理 データープラットフォーム 技術との統合 Content Img … Author … Client B Client A 1: Ch9, Jan-2, 5 2: If-Match: If-Match: 1 1 Version Rating 1: Ch9, Jan-2, 4 Error: 412 5 : Ch9, Jan-1, 3 Ch9, Jan-2, 4 Ch9, Jan-2, 5 1 : Ch9, 2: Ch9,Jan-2, Jan-2,52 9 : Ch9, Jan-3, 6 一般的な HTTP の仕組みを利用 – Etag と If-Match エンティティの取得 – Etag を返却 エンティティの更新 – レーティングの変更 更新のバージョンチェック - IF-Match と Etag 成功の場合, Client-A により更新 バージョンが一致しない場合、指定された条件に合致しないエ ラー (412) を返す 上位 N エンティティの取得 .NET: LINQ Take(N) ファンクション serviceUri = new Uri("http://<account>.table.core.windows.net"); DataServiceContext context = new DataServiceContext(serviceUri); var allMessages = context.CreateQuery<Message>("Messages"); foreach (Message message in allMessages.Take(100)) { Console.WriteLine(message.Name); } GET http://<serviceUri>/Messages?$top=100 N+1番目~継続トークン リクエストの送信 GET http://<serviceUri>/Messages?$filter=...&$top=100 x-ms-continuation-NextPartitionKey: Channel9 x-ms-continuation-NextRowKey: Date101 100 Ch9, Date1, GET http://<Uri>/Messages?$filter=...&$top=100 &NextPartitionKey=Channel9 &NextRowKey=Date101 Ch9, Date2, Ch9, … Ch9,Date100, Ch9,Date101, Ch9, … Messages 単一エンティティCUD での ACID トラン ザクション Insert/update/delete シングルパーティション内のクエリへの スナップショットアイソレーション クエリ開始時から一貫したビュー ダーティーリードなし 同時アップデートをブロックしない パーティション間のスナップショットア イソレーションなし 他の継続クエリ間でのスナップショット アイソレーションなし アプリケーションは一貫性の維持の責 務を負う 処理途中でのエラーの発生への対処 例: メッセージ削除後のアプリケーション エラー Windows Azure ストレージ(キュー)を 用いて、操作の完了を確保する仕組みを作 る Delete channel Front end Delete messages worker Worker Queue Front End 2 Del Ch1 Del Ch11 Del Ch5 Ch1, Msg1 Ch1, Msg2 Ch1, Msg3 Ch2, Msg1 Ch2, Msg2 Ch1,… Ch3, Msg1 Ch2,… Messages Channels 2. キューのエントリー削除 3. 4. メッセージの削除 ChannelsからCh1の削除 1. Dequeue DelCh1 Front end Delete messages worker Front End 2 Del Ch1 Del Ch11 Del Ch5 Queue Del Ch1h Delete channel Worker 1 Ch1, Msg1 Ch1, Msg2 Ch1, Msg3 Worker2 Ch2, Msg1 Ch2, Msg2 Ch1,… Ch3, Msg1 Ch2,… Messages Channels 5. 1. 削除を再度行う 2. 3. 4. Dequeue Ch1とMsg1削除後のエラー DelCh1 がキュー上で復活 DelCh1、削除開始 DelCh1 を行う Windows Azure が提供するもの クラウドコンピュー ティング Web ロール Webアプリケーション Worker ロール .NET プロセス クラウドストレージ ブロブ バイナリデータの集合 テーブル 構造化データ キュー キュー内のメッセージ Windows Azure ストレージ Windows Azure ストレージはエッセンシャ ルクラウドストレージ 堅牢性、拡張性、可用性 セキュリティ、パフォーマンス キュー、テーブル、ブロブの3種類のストア方 式を提供 シンプルで使いなれたプログラミングイン ターフェイス REST と ADO.NET ブロブ テーブル キュー Windows Azure ストレージ概念 コンテナ ブロブ http://<account>.blob.core.windows.net/<container> アカウント テーブル エンティティ http://<account>.table.core.windows.net/<table> キュー メッセージ http://<account>.queue.core.windows.net/<queue> Windows Azure http://www.azure.com/windows ADO.NET Data Services http://blogs.msdn.com/astoriateam MSDN Developer Center http://msdn.microsoft.com/en-us/azure MSDN Forum http://social.msdn.microsoft.com/Forums/ en-US/category/azure/ アカウント コンテナ ブロブ名 PUT http://dvd.blob.core.windows.net/movies/TheBlob.wmv ?comp=block &blockid=BlockId1 &timeout=60 HTTP/1.1 Content-Length: 4194304 Content-MD5: HUXZLQLMuI/KZ5KDcJPcOA== Authorization: SharedKey dvd: F5a+dUDvef+PfMb4T8Rc2jHcwfK58KecSZY+l2naIao= x-ms-date: Mon, 27 Oct 2008 17:00:25 GMT ……… Block Data Contents ……… アカウント コンテナ ブロブ名 PUT http://dvd.blob.core.windows.net/movies/TheBlob.wmv ?comp=blocklist &timeout=120 HTTP/1.1 Content-Length: 161213 Authorization: SharedKey dvd: QrmowAF72IsFEs0GaNCtRU143JpkflIgRTcOdKZaYxw= x-ms-date: Mon, 27 Oct 2008 17:00:25 GMT <?xml version=“1.0” encoding=“utf-8”?> <BlockList> <Block>BlockId1</Block> <Block>BlockId2</Block> ……………… </BlockList> • ブロブ全体を取得 GET http://dvd.blob.core.windows.net/movies/TheBlob.wmv HTTP/1.1 Authorization: SharedKey dvd: RGllHMtzKMi4y/nedSk5Vn74IU6/fRMwiPsL+uYSDjY= X-ms-date: Mon, 27 Oct 2008 17:00:25 GMT • ブロブの一部を取得 GET http://dvd.blob.core.windows.net/movies/TheBlob.wmv ?timeout=60 HTTP/1.1 Range: bytes=1024000-2048000 コンテナ内のブロブの一覧を列挙 階層的なリスティング: prefix + delimiter 連続: NextMarker + Marker List top level “directories” Containerコンテナ “movies” “movies” : has: Action/Rocky1.wmv Action/Rocky.avi Action/Rocky2.avi Action/Rocky2.wmv Drama/Crime/GodFather.avi Action/Rocky3.wmv Drama/Crime/GodFather2.avi Action/Rocky4.wmv Drama/LordOfRings.avi Action/Rocky5.wmv Thriller/TheBlob.wmv Drama/Crime/GodFather1.wmv Drama/Crime/GodFather2.wmv Drama/Memento.wmv Horror/TheBlob.wmv REST Request: GET http://dvd.blob.windows.net/movies ?comp=list &delimiter=/ Results: <BlobPrefix>Action</BlobPrefix> <BlobPrefix>Drama</BlobPrefix> <BlobPrefix>Horror</BlobPrefix> コンテナ内のブロブの一覧を列挙 階層的なリスティング: prefix + delimiter 連続: NextMarker + Marker List directory “Drama”: Containerコンテナ “movies” “movies” : has: Action/Rocky1.wmv Action/Rocky1.avi Action/Rocky2.avi Action/Rocky2.wmv ……… Action/Rocky3.wmv Action/Rocky5.avi Action/Rocky4.wmv Drama/Crime/GodFather1.avi Action/Rocky5.wmv Drama/Crime/GodFather2.avi Drama/Crime/GodFather1.wmv Drama/Gladiator.avi Drama/Crime/GodFather2.wmv Horror/TheBlob.wmv Drama/Memento.wmv Horror/TheBlob.wmv REST Request: GET http://dvd.blob.windows.net/movies ?comp=list &prefix=Drama/ &delimiter=/ Results: <BlobPrefix>Drama/Crime</BlobPrefix> <Blob>Drama/Memento.wmv</Blob> コンテナ内のブロブの一覧を列挙 階層的なリスティング: prefix + delimiter 連続: NextMarker + Marker Max Results and Next Marker “movies” コンテナ: Action/Rocky1.wmv Action/Rocky2.wmv Action/Rocky3.wmv Action/Rocky4.wmv Action/Rocky5.wmv Drama/Crime/GodFather1.wmv Drama/Crime/GodFather2.wmv Drama/Memento.wmv Horror/TheBlob.wmv REST Request: GET http://dvd.blob.windows.net/movies ?comp=list &prefix=Action &maxresults=3 Results: <Blob>Action/Rocky1.wmv</Blob> <Blob>Action/Rocky2.wmv</Blob> <Blob>Action/Rocky3.wmv</Blob> <NextMarker>OpaqueMarker1</NextMarker> コンテナ内のブロブの一覧を列挙 階層的なリスティング: prefix + delimiter 連続: NextMarker + Marker Using Continuation Marker “movies” コンテナ: Action/Rocky1.wmv Action/Rocky2.wmv Action/Rocky3.wmv Action/Rocky4.wmv Action/Rocky5.wmv Drama/Crime/GodFather1.wmv Drama/Crime/GodFather2.wmv Drama/Memento.wmv Horror/TheBlob.wmv REST Request: GET http://dvd.blob.windows.net/movies ?comp=list &prefix=Action &maxresults=3 &marker=OpaqueMarker1 Results: <Blob>Action/Rocky4.wmv</Blob> <Blob>Action/Rocky5.wmv</Blob> <NextMarker></NextMarker> .NETクラスとしてスキーマ(エンティティ)を定義 [DataServiceKey("PartitionKey", "RowKey")] public class Customer { // Partition key – Customer Last name public string PartitionKey { get; set; } // Row Key – Customer First name public string RowKey { get; set; } // User defined properties here public DateTime CustomerSince { get; set; } public double Rating { get; set; } } public string Occupation { get; set; } テーブル名 "Customers” アカウントに “Tables”と呼ばれるマスターテーブル アカウント内の管理に利用さえる テーブルを用いるとき、 “Tables” へテーブルを追加 [DataServiceKey("TableName")] public class TableStorageTable { public string TableName { get; set; } } // serviceUri is “http://<Account>.table.core.windows.net/” DataServiceContext context = new DataServiceContext(serviceUri); TableStorageTable table = new TableStorageTable("Customers"); context.AddObject("Tables", table); DataServiceResponse response = context.SaveChanges(); テーブルにエンティティを挿入 Customer cust = new Customer( “Lee”, // Partition Key = Last Name “Geddy”, // Row Key = First Name DateTime.UtcNow, // Customer Since 2.0, // Rating “Engineer” // Occupation); // Service Uri is “http://<Account>.table.core.windows.net/” DataServiceContext context = new DataServiceContext(serviceUri); context.AddObject(“Customers”, cust); DataServiceResponse response = context.SaveChanges(); LINQ DataServiceContext context = new DataServiceContext(“http://myaccount.table.core.windows.net”); var customers = from o in context.CreateQuery<Customer>(“Customers”) where o.PartitionKey == “Lee” select o; foreach (Customer customer in customers) { } GET http://myaccount.table.core.windows.net/Customers? $filter= PartitionKey eq ‘Lee’ Customer cust = ( from c in context.CreateQuery<Customer> (“Customers”) where c.PartitionKey == “Lee” // Partition Key = Last Name && c.RowKey == “Geddy” // Row Key = First Name select c) .FirstOrDefault(); cust.Occupation = “Musician”; context.UpdateObject(cust); DataServiceResponse response = context.SaveChanges(); context.DeleteObject(cust); DataServiceResponse response = context.SaveChanges(); © 2009 Microsoft Corporation. All rights reserved. Microsoft, Windows, Windows Vista and other product names are or may be registered trademarks and/or trademarks in the U.S. and/or other countries. The information herein is for informational purposes only and represents the current view of Microsoft Corporation as of the date of this presentation. Because Microsoft must respond to changing market conditions, it should not be interpreted to be a commitment on the part of Microsoft, and Microsoft cannot guarantee the accuracy of any information provided after the date of this presentation. MICROSOFT MAKES NO WARRANTIES, EXPRESS, IMPLIED OR STATUTORY, AS TO THE INFORMATION IN THIS PRESENTATION.
© Copyright 2025 ExpyDoc