OpenStack Image Management (Glance)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

test_images.py 232KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441
  1. # Copyright 2012 OpenStack Foundation
  2. # All Rights Reserved.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  5. # not use this file except in compliance with the License. You may obtain
  6. # 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, WITHOUT
  12. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. # License for the specific language governing permissions and limitations
  14. # under the License.
  15. import hashlib
  16. import os
  17. import signal
  18. import uuid
  19. from oslo_serialization import jsonutils
  20. import requests
  21. import six
  22. from six.moves import http_client as http
  23. # NOTE(jokke): simplified transition to py3, behaves like py2 xrange
  24. from six.moves import range
  25. from six.moves import urllib
  26. from glance.tests import functional
  27. from glance.tests.functional import ft_utils as func_utils
  28. from glance.tests import utils as test_utils
  29. TENANT1 = str(uuid.uuid4())
  30. TENANT2 = str(uuid.uuid4())
  31. TENANT3 = str(uuid.uuid4())
  32. TENANT4 = str(uuid.uuid4())
  33. class TestImages(functional.FunctionalTest):
  34. def setUp(self):
  35. super(TestImages, self).setUp()
  36. self.cleanup()
  37. self.include_scrubber = False
  38. self.api_server.deployment_flavor = 'noauth'
  39. self.api_server.data_api = 'glance.db.sqlalchemy.api'
  40. for i in range(3):
  41. ret = test_utils.start_http_server("foo_image_id%d" % i,
  42. "foo_image%d" % i)
  43. setattr(self, 'http_server%d_pid' % i, ret[0])
  44. setattr(self, 'http_port%d' % i, ret[1])
  45. self.api_server.use_user_token = True
  46. self.api_server.send_identity_credentials = True
  47. def tearDown(self):
  48. for i in range(3):
  49. pid = getattr(self, 'http_server%d_pid' % i, None)
  50. if pid:
  51. os.kill(pid, signal.SIGKILL)
  52. super(TestImages, self).tearDown()
  53. def _url(self, path):
  54. return 'http://127.0.0.1:%d%s' % (self.api_port, path)
  55. def _headers(self, custom_headers=None):
  56. base_headers = {
  57. 'X-Identity-Status': 'Confirmed',
  58. 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96',
  59. 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e',
  60. 'X-Tenant-Id': TENANT1,
  61. 'X-Roles': 'member',
  62. }
  63. base_headers.update(custom_headers or {})
  64. return base_headers
  65. def test_not_authenticated_in_registry_on_ops(self):
  66. # https://bugs.launchpad.net/glance/+bug/1451850
  67. # this configuration guarantees that authentication succeeds in
  68. # glance-api and fails in glance-registry if no token is passed
  69. self.api_server.deployment_flavor = ''
  70. # make sure that request will reach registry
  71. self.api_server.data_api = 'glance.db.registry.api'
  72. self.registry_server.deployment_flavor = 'fakeauth'
  73. self.start_servers(**self.__dict__.copy())
  74. headers = {'content-type': 'application/json'}
  75. image = {'name': 'image', 'type': 'kernel', 'disk_format': 'qcow2',
  76. 'container_format': 'bare'}
  77. # image create should return 401
  78. response = requests.post(self._url('/v2/images'), headers=headers,
  79. data=jsonutils.dumps(image))
  80. self.assertEqual(http.UNAUTHORIZED, response.status_code)
  81. # image list should return 401
  82. response = requests.get(self._url('/v2/images'))
  83. self.assertEqual(http.UNAUTHORIZED, response.status_code)
  84. # image show should return 401
  85. response = requests.get(self._url('/v2/images/someimageid'))
  86. self.assertEqual(http.UNAUTHORIZED, response.status_code)
  87. # image update should return 401
  88. ops = [{'op': 'replace', 'path': '/protected', 'value': False}]
  89. media_type = 'application/openstack-images-v2.1-json-patch'
  90. response = requests.patch(self._url('/v2/images/someimageid'),
  91. headers={'content-type': media_type},
  92. data=jsonutils.dumps(ops))
  93. self.assertEqual(http.UNAUTHORIZED, response.status_code)
  94. # image delete should return 401
  95. response = requests.delete(self._url('/v2/images/someimageid'))
  96. self.assertEqual(http.UNAUTHORIZED, response.status_code)
  97. self.stop_servers()
  98. def test_image_import_using_glance_direct(self):
  99. self.start_servers(**self.__dict__.copy())
  100. # Image list should be empty
  101. path = self._url('/v2/images')
  102. response = requests.get(path, headers=self._headers())
  103. self.assertEqual(http.OK, response.status_code)
  104. images = jsonutils.loads(response.text)['images']
  105. self.assertEqual(0, len(images))
  106. # glance-direct should be available in discovery response
  107. path = self._url('/v2/info/import')
  108. response = requests.get(path, headers=self._headers())
  109. self.assertEqual(http.OK, response.status_code)
  110. discovery_calls = jsonutils.loads(
  111. response.text)['import-methods']['value']
  112. self.assertIn("glance-direct", discovery_calls)
  113. # Create an image
  114. path = self._url('/v2/images')
  115. headers = self._headers({'content-type': 'application/json'})
  116. data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel',
  117. 'disk_format': 'aki',
  118. 'container_format': 'aki'})
  119. response = requests.post(path, headers=headers, data=data)
  120. self.assertEqual(http.CREATED, response.status_code)
  121. # Returned image entity should have a generated id and status
  122. image = jsonutils.loads(response.text)
  123. image_id = image['id']
  124. checked_keys = set([
  125. u'status',
  126. u'name',
  127. u'tags',
  128. u'created_at',
  129. u'updated_at',
  130. u'visibility',
  131. u'self',
  132. u'protected',
  133. u'os_hidden',
  134. u'id',
  135. u'file',
  136. u'min_disk',
  137. u'type',
  138. u'min_ram',
  139. u'schema',
  140. u'disk_format',
  141. u'container_format',
  142. u'owner',
  143. u'checksum',
  144. u'os_hash_algo',
  145. u'os_hash_value',
  146. u'size',
  147. u'virtual_size',
  148. ])
  149. self.assertEqual(checked_keys, set(image.keys()))
  150. expected_image = {
  151. 'status': 'queued',
  152. 'name': 'image-1',
  153. 'tags': [],
  154. 'visibility': 'shared',
  155. 'self': '/v2/images/%s' % image_id,
  156. 'protected': False,
  157. 'file': '/v2/images/%s/file' % image_id,
  158. 'min_disk': 0,
  159. 'type': 'kernel',
  160. 'min_ram': 0,
  161. 'schema': '/v2/schemas/image',
  162. }
  163. for key, value in expected_image.items():
  164. self.assertEqual(value, image[key], key)
  165. # Image list should now have one entry
  166. path = self._url('/v2/images')
  167. response = requests.get(path, headers=self._headers())
  168. self.assertEqual(http.OK, response.status_code)
  169. images = jsonutils.loads(response.text)['images']
  170. self.assertEqual(1, len(images))
  171. self.assertEqual(image_id, images[0]['id'])
  172. # Upload some image data to staging area
  173. path = self._url('/v2/images/%s/stage' % image_id)
  174. headers = self._headers({'Content-Type': 'application/octet-stream'})
  175. image_data = b'ZZZZZ'
  176. response = requests.put(path, headers=headers, data=image_data)
  177. self.assertEqual(http.NO_CONTENT, response.status_code)
  178. # Verify image is in uploading state, hashes are None
  179. func_utils.verify_image_hashes_and_status(self, image_id,
  180. status='uploading')
  181. # Import image to store
  182. path = self._url('/v2/images/%s/import' % image_id)
  183. headers = self._headers({
  184. 'content-type': 'application/json',
  185. 'X-Roles': 'admin',
  186. })
  187. data = jsonutils.dumps({'method': {
  188. 'name': 'glance-direct'
  189. }})
  190. response = requests.post(path, headers=headers, data=data)
  191. self.assertEqual(http.ACCEPTED, response.status_code)
  192. # Verify image is in active state and checksum is set
  193. # NOTE(abhishekk): As import is a async call we need to provide
  194. # some timelap to complete the call.
  195. path = self._url('/v2/images/%s' % image_id)
  196. func_utils.wait_for_status(request_path=path,
  197. request_headers=self._headers(),
  198. status='active',
  199. max_sec=2,
  200. delay_sec=0.2)
  201. expect_c = six.text_type(hashlib.md5(image_data).hexdigest())
  202. expect_h = six.text_type(hashlib.sha512(image_data).hexdigest())
  203. func_utils.verify_image_hashes_and_status(self,
  204. image_id,
  205. checksum=expect_c,
  206. os_hash_value=expect_h,
  207. status='active')
  208. # Ensure the size is updated to reflect the data uploaded
  209. path = self._url('/v2/images/%s' % image_id)
  210. response = requests.get(path, headers=self._headers())
  211. self.assertEqual(http.OK, response.status_code)
  212. self.assertEqual(5, jsonutils.loads(response.text)['size'])
  213. # Deleting image should work
  214. path = self._url('/v2/images/%s' % image_id)
  215. response = requests.delete(path, headers=self._headers())
  216. self.assertEqual(http.NO_CONTENT, response.status_code)
  217. # Image list should now be empty
  218. path = self._url('/v2/images')
  219. response = requests.get(path, headers=self._headers())
  220. self.assertEqual(http.OK, response.status_code)
  221. images = jsonutils.loads(response.text)['images']
  222. self.assertEqual(0, len(images))
  223. self.stop_servers()
  224. def test_image_import_using_web_download(self):
  225. self.config(node_staging_uri="file:///tmp/staging/")
  226. self.start_servers(**self.__dict__.copy())
  227. # Image list should be empty
  228. path = self._url('/v2/images')
  229. response = requests.get(path, headers=self._headers())
  230. self.assertEqual(http.OK, response.status_code)
  231. images = jsonutils.loads(response.text)['images']
  232. self.assertEqual(0, len(images))
  233. # web-download should be available in discovery response
  234. path = self._url('/v2/info/import')
  235. response = requests.get(path, headers=self._headers())
  236. self.assertEqual(http.OK, response.status_code)
  237. discovery_calls = jsonutils.loads(
  238. response.text)['import-methods']['value']
  239. self.assertIn("web-download", discovery_calls)
  240. # Create an image
  241. path = self._url('/v2/images')
  242. headers = self._headers({'content-type': 'application/json'})
  243. data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel',
  244. 'disk_format': 'aki',
  245. 'container_format': 'aki'})
  246. response = requests.post(path, headers=headers, data=data)
  247. self.assertEqual(http.CREATED, response.status_code)
  248. # Returned image entity should have a generated id and status
  249. image = jsonutils.loads(response.text)
  250. image_id = image['id']
  251. checked_keys = set([
  252. u'status',
  253. u'name',
  254. u'tags',
  255. u'created_at',
  256. u'updated_at',
  257. u'visibility',
  258. u'self',
  259. u'protected',
  260. u'os_hidden',
  261. u'id',
  262. u'file',
  263. u'min_disk',
  264. u'type',
  265. u'min_ram',
  266. u'schema',
  267. u'disk_format',
  268. u'container_format',
  269. u'owner',
  270. u'checksum',
  271. u'os_hash_algo',
  272. u'os_hash_value',
  273. u'size',
  274. u'virtual_size',
  275. ])
  276. self.assertEqual(checked_keys, set(image.keys()))
  277. expected_image = {
  278. 'status': 'queued',
  279. 'name': 'image-1',
  280. 'tags': [],
  281. 'visibility': 'shared',
  282. 'self': '/v2/images/%s' % image_id,
  283. 'protected': False,
  284. 'file': '/v2/images/%s/file' % image_id,
  285. 'min_disk': 0,
  286. 'type': 'kernel',
  287. 'min_ram': 0,
  288. 'schema': '/v2/schemas/image',
  289. }
  290. for key, value in expected_image.items():
  291. self.assertEqual(value, image[key], key)
  292. # Image list should now have one entry
  293. path = self._url('/v2/images')
  294. response = requests.get(path, headers=self._headers())
  295. self.assertEqual(http.OK, response.status_code)
  296. images = jsonutils.loads(response.text)['images']
  297. self.assertEqual(1, len(images))
  298. self.assertEqual(image_id, images[0]['id'])
  299. # Verify image is in queued state and hashes are None
  300. func_utils.verify_image_hashes_and_status(self,
  301. image_id,
  302. status='queued')
  303. # Import image to store
  304. path = self._url('/v2/images/%s/import' % image_id)
  305. headers = self._headers({
  306. 'content-type': 'application/json',
  307. 'X-Roles': 'admin',
  308. })
  309. image_data_uri = ('https://www.openstack.org/assets/openstack-logo/'
  310. '2016R/OpenStack-Logo-Horizontal.eps.zip')
  311. data = jsonutils.dumps({'method': {
  312. 'name': 'web-download',
  313. 'uri': image_data_uri
  314. }})
  315. response = requests.post(path, headers=headers, data=data)
  316. self.assertEqual(http.ACCEPTED, response.status_code)
  317. # Verify image is in active state and checksum is set
  318. # NOTE(abhishekk): As import is a async call we need to provide
  319. # some timelap to complete the call.
  320. path = self._url('/v2/images/%s' % image_id)
  321. func_utils.wait_for_status(request_path=path,
  322. request_headers=self._headers(),
  323. status='active',
  324. max_sec=20,
  325. delay_sec=0.2,
  326. start_delay_sec=1)
  327. with requests.get(image_data_uri) as r:
  328. expect_c = six.text_type(hashlib.md5(r.content).hexdigest())
  329. expect_h = six.text_type(hashlib.sha512(r.content).hexdigest())
  330. func_utils.verify_image_hashes_and_status(self,
  331. image_id,
  332. checksum=expect_c,
  333. os_hash_value=expect_h,
  334. status='active')
  335. # Deleting image should work
  336. path = self._url('/v2/images/%s' % image_id)
  337. response = requests.delete(path, headers=self._headers())
  338. self.assertEqual(http.NO_CONTENT, response.status_code)
  339. # Image list should now be empty
  340. path = self._url('/v2/images')
  341. response = requests.get(path, headers=self._headers())
  342. self.assertEqual(http.OK, response.status_code)
  343. images = jsonutils.loads(response.text)['images']
  344. self.assertEqual(0, len(images))
  345. self.stop_servers()
  346. def test_image_lifecycle(self):
  347. # Image list should be empty
  348. self.api_server.show_multiple_locations = True
  349. self.start_servers(**self.__dict__.copy())
  350. path = self._url('/v2/images')
  351. response = requests.get(path, headers=self._headers())
  352. self.assertEqual(http.OK, response.status_code)
  353. images = jsonutils.loads(response.text)['images']
  354. self.assertEqual(0, len(images))
  355. # Create an image (with two deployer-defined properties)
  356. path = self._url('/v2/images')
  357. headers = self._headers({'content-type': 'application/json'})
  358. data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel',
  359. 'foo': 'bar', 'disk_format': 'aki',
  360. 'container_format': 'aki', 'abc': 'xyz',
  361. 'protected': True})
  362. response = requests.post(path, headers=headers, data=data)
  363. self.assertEqual(http.CREATED, response.status_code)
  364. image_location_header = response.headers['Location']
  365. # Returned image entity should have a generated id and status
  366. image = jsonutils.loads(response.text)
  367. image_id = image['id']
  368. checked_keys = set([
  369. u'status',
  370. u'name',
  371. u'tags',
  372. u'created_at',
  373. u'updated_at',
  374. u'visibility',
  375. u'self',
  376. u'protected',
  377. u'os_hidden',
  378. u'id',
  379. u'file',
  380. u'min_disk',
  381. u'foo',
  382. u'abc',
  383. u'type',
  384. u'min_ram',
  385. u'schema',
  386. u'disk_format',
  387. u'container_format',
  388. u'owner',
  389. u'checksum',
  390. u'os_hash_algo',
  391. u'os_hash_value',
  392. u'size',
  393. u'virtual_size',
  394. u'locations',
  395. ])
  396. self.assertEqual(checked_keys, set(image.keys()))
  397. expected_image = {
  398. 'status': 'queued',
  399. 'name': 'image-1',
  400. 'tags': [],
  401. 'visibility': 'shared',
  402. 'self': '/v2/images/%s' % image_id,
  403. 'protected': True,
  404. 'file': '/v2/images/%s/file' % image_id,
  405. 'min_disk': 0,
  406. 'foo': 'bar',
  407. 'abc': 'xyz',
  408. 'type': 'kernel',
  409. 'min_ram': 0,
  410. 'schema': '/v2/schemas/image',
  411. }
  412. for key, value in expected_image.items():
  413. self.assertEqual(value, image[key], key)
  414. # Image list should now have one entry
  415. path = self._url('/v2/images')
  416. response = requests.get(path, headers=self._headers())
  417. self.assertEqual(http.OK, response.status_code)
  418. images = jsonutils.loads(response.text)['images']
  419. self.assertEqual(1, len(images))
  420. self.assertEqual(image_id, images[0]['id'])
  421. # Create another image (with two deployer-defined properties)
  422. path = self._url('/v2/images')
  423. headers = self._headers({'content-type': 'application/json'})
  424. data = jsonutils.dumps({'name': 'image-2', 'type': 'kernel',
  425. 'bar': 'foo', 'disk_format': 'aki',
  426. 'container_format': 'aki', 'xyz': 'abc'})
  427. response = requests.post(path, headers=headers, data=data)
  428. self.assertEqual(http.CREATED, response.status_code)
  429. # Returned image entity should have a generated id and status
  430. image = jsonutils.loads(response.text)
  431. image2_id = image['id']
  432. checked_keys = set([
  433. u'status',
  434. u'name',
  435. u'tags',
  436. u'created_at',
  437. u'updated_at',
  438. u'visibility',
  439. u'self',
  440. u'protected',
  441. u'os_hidden',
  442. u'id',
  443. u'file',
  444. u'min_disk',
  445. u'bar',
  446. u'xyz',
  447. u'type',
  448. u'min_ram',
  449. u'schema',
  450. u'disk_format',
  451. u'container_format',
  452. u'owner',
  453. u'checksum',
  454. u'os_hash_algo',
  455. u'os_hash_value',
  456. u'size',
  457. u'virtual_size',
  458. u'locations',
  459. ])
  460. self.assertEqual(checked_keys, set(image.keys()))
  461. expected_image = {
  462. 'status': 'queued',
  463. 'name': 'image-2',
  464. 'tags': [],
  465. 'visibility': 'shared',
  466. 'self': '/v2/images/%s' % image2_id,
  467. 'protected': False,
  468. 'file': '/v2/images/%s/file' % image2_id,
  469. 'min_disk': 0,
  470. 'bar': 'foo',
  471. 'xyz': 'abc',
  472. 'type': 'kernel',
  473. 'min_ram': 0,
  474. 'schema': '/v2/schemas/image',
  475. }
  476. for key, value in expected_image.items():
  477. self.assertEqual(value, image[key], key)
  478. # Image list should now have two entries
  479. path = self._url('/v2/images')
  480. response = requests.get(path, headers=self._headers())
  481. self.assertEqual(http.OK, response.status_code)
  482. images = jsonutils.loads(response.text)['images']
  483. self.assertEqual(2, len(images))
  484. self.assertEqual(image2_id, images[0]['id'])
  485. self.assertEqual(image_id, images[1]['id'])
  486. # Image list should list only image-2 as image-1 doesn't contain the
  487. # property 'bar'
  488. path = self._url('/v2/images?bar=foo')
  489. response = requests.get(path, headers=self._headers())
  490. self.assertEqual(http.OK, response.status_code)
  491. images = jsonutils.loads(response.text)['images']
  492. self.assertEqual(1, len(images))
  493. self.assertEqual(image2_id, images[0]['id'])
  494. # Image list should list only image-1 as image-2 doesn't contain the
  495. # property 'foo'
  496. path = self._url('/v2/images?foo=bar')
  497. response = requests.get(path, headers=self._headers())
  498. self.assertEqual(http.OK, response.status_code)
  499. images = jsonutils.loads(response.text)['images']
  500. self.assertEqual(1, len(images))
  501. self.assertEqual(image_id, images[0]['id'])
  502. # The "changes-since" filter shouldn't work on glance v2
  503. path = self._url('/v2/images?changes-since=20001007T10:10:10')
  504. response = requests.get(path, headers=self._headers())
  505. self.assertEqual(http.BAD_REQUEST, response.status_code)
  506. path = self._url('/v2/images?changes-since=aaa')
  507. response = requests.get(path, headers=self._headers())
  508. self.assertEqual(http.BAD_REQUEST, response.status_code)
  509. # Image list should list only image-1 based on the filter
  510. # 'protected=true'
  511. path = self._url('/v2/images?protected=true')
  512. response = requests.get(path, headers=self._headers())
  513. self.assertEqual(http.OK, response.status_code)
  514. images = jsonutils.loads(response.text)['images']
  515. self.assertEqual(1, len(images))
  516. self.assertEqual(image_id, images[0]['id'])
  517. # Image list should list only image-2 based on the filter
  518. # 'protected=false'
  519. path = self._url('/v2/images?protected=false')
  520. response = requests.get(path, headers=self._headers())
  521. self.assertEqual(http.OK, response.status_code)
  522. images = jsonutils.loads(response.text)['images']
  523. self.assertEqual(1, len(images))
  524. self.assertEqual(image2_id, images[0]['id'])
  525. # Image list should return 400 based on the filter
  526. # 'protected=False'
  527. path = self._url('/v2/images?protected=False')
  528. response = requests.get(path, headers=self._headers())
  529. self.assertEqual(http.BAD_REQUEST, response.status_code)
  530. # Image list should list only image-1 based on the filter
  531. # 'foo=bar&abc=xyz'
  532. path = self._url('/v2/images?foo=bar&abc=xyz')
  533. response = requests.get(path, headers=self._headers())
  534. self.assertEqual(http.OK, response.status_code)
  535. images = jsonutils.loads(response.text)['images']
  536. self.assertEqual(1, len(images))
  537. self.assertEqual(image_id, images[0]['id'])
  538. # Image list should list only image-2 based on the filter
  539. # 'bar=foo&xyz=abc'
  540. path = self._url('/v2/images?bar=foo&xyz=abc')
  541. response = requests.get(path, headers=self._headers())
  542. self.assertEqual(http.OK, response.status_code)
  543. images = jsonutils.loads(response.text)['images']
  544. self.assertEqual(1, len(images))
  545. self.assertEqual(image2_id, images[0]['id'])
  546. # Image list should not list anything as the filter 'foo=baz&abc=xyz'
  547. # is not satisfied by either images
  548. path = self._url('/v2/images?foo=baz&abc=xyz')
  549. response = requests.get(path, headers=self._headers())
  550. self.assertEqual(http.OK, response.status_code)
  551. images = jsonutils.loads(response.text)['images']
  552. self.assertEqual(0, len(images))
  553. # Get the image using the returned Location header
  554. response = requests.get(image_location_header, headers=self._headers())
  555. self.assertEqual(http.OK, response.status_code)
  556. image = jsonutils.loads(response.text)
  557. self.assertEqual(image_id, image['id'])
  558. self.assertIsNone(image['checksum'])
  559. self.assertIsNone(image['size'])
  560. self.assertIsNone(image['virtual_size'])
  561. self.assertEqual('bar', image['foo'])
  562. self.assertTrue(image['protected'])
  563. self.assertEqual('kernel', image['type'])
  564. self.assertTrue(image['created_at'])
  565. self.assertTrue(image['updated_at'])
  566. self.assertEqual(image['updated_at'], image['created_at'])
  567. # The URI file:// should return a 400 rather than a 500
  568. path = self._url('/v2/images/%s' % image_id)
  569. media_type = 'application/openstack-images-v2.1-json-patch'
  570. headers = self._headers({'content-type': media_type})
  571. url = ('file://')
  572. changes = [{
  573. 'op': 'add',
  574. 'path': '/locations/-',
  575. 'value': {
  576. 'url': url,
  577. 'metadata': {}
  578. }
  579. }]
  580. data = jsonutils.dumps(changes)
  581. response = requests.patch(path, headers=headers, data=data)
  582. self.assertEqual(http.BAD_REQUEST, response.status_code, response.text)
  583. # The image should be mutable, including adding and removing properties
  584. path = self._url('/v2/images/%s' % image_id)
  585. media_type = 'application/openstack-images-v2.1-json-patch'
  586. headers = self._headers({'content-type': media_type})
  587. data = jsonutils.dumps([
  588. {'op': 'replace', 'path': '/name', 'value': 'image-2'},
  589. {'op': 'replace', 'path': '/disk_format', 'value': 'vhd'},
  590. {'op': 'replace', 'path': '/container_format', 'value': 'ami'},
  591. {'op': 'replace', 'path': '/foo', 'value': 'baz'},
  592. {'op': 'add', 'path': '/ping', 'value': 'pong'},
  593. {'op': 'replace', 'path': '/protected', 'value': True},
  594. {'op': 'remove', 'path': '/type'},
  595. ])
  596. response = requests.patch(path, headers=headers, data=data)
  597. self.assertEqual(http.OK, response.status_code, response.text)
  598. # Returned image entity should reflect the changes
  599. image = jsonutils.loads(response.text)
  600. self.assertEqual('image-2', image['name'])
  601. self.assertEqual('vhd', image['disk_format'])
  602. self.assertEqual('baz', image['foo'])
  603. self.assertEqual('pong', image['ping'])
  604. self.assertTrue(image['protected'])
  605. self.assertNotIn('type', image, response.text)
  606. # Adding 11 image properties should fail since configured limit is 10
  607. path = self._url('/v2/images/%s' % image_id)
  608. media_type = 'application/openstack-images-v2.1-json-patch'
  609. headers = self._headers({'content-type': media_type})
  610. changes = []
  611. for i in range(11):
  612. changes.append({'op': 'add',
  613. 'path': '/ping%i' % i,
  614. 'value': 'pong'})
  615. data = jsonutils.dumps(changes)
  616. response = requests.patch(path, headers=headers, data=data)
  617. self.assertEqual(http.REQUEST_ENTITY_TOO_LARGE, response.status_code,
  618. response.text)
  619. # Adding 3 image locations should fail since configured limit is 2
  620. path = self._url('/v2/images/%s' % image_id)
  621. media_type = 'application/openstack-images-v2.1-json-patch'
  622. headers = self._headers({'content-type': media_type})
  623. changes = []
  624. for i in range(3):
  625. url = ('http://127.0.0.1:%s/foo_image' %
  626. getattr(self, 'http_port%d' % i))
  627. changes.append({'op': 'add', 'path': '/locations/-',
  628. 'value': {'url': url, 'metadata': {}},
  629. })
  630. data = jsonutils.dumps(changes)
  631. response = requests.patch(path, headers=headers, data=data)
  632. self.assertEqual(http.REQUEST_ENTITY_TOO_LARGE, response.status_code,
  633. response.text)
  634. # Ensure the v2.0 json-patch content type is accepted
  635. path = self._url('/v2/images/%s' % image_id)
  636. media_type = 'application/openstack-images-v2.0-json-patch'
  637. headers = self._headers({'content-type': media_type})
  638. data = jsonutils.dumps([{'add': '/ding', 'value': 'dong'}])
  639. response = requests.patch(path, headers=headers, data=data)
  640. self.assertEqual(http.OK, response.status_code, response.text)
  641. # Returned image entity should reflect the changes
  642. image = jsonutils.loads(response.text)
  643. self.assertEqual('dong', image['ding'])
  644. # Updates should persist across requests
  645. path = self._url('/v2/images/%s' % image_id)
  646. response = requests.get(path, headers=self._headers())
  647. self.assertEqual(http.OK, response.status_code)
  648. image = jsonutils.loads(response.text)
  649. self.assertEqual(image_id, image['id'])
  650. self.assertEqual('image-2', image['name'])
  651. self.assertEqual('baz', image['foo'])
  652. self.assertEqual('pong', image['ping'])
  653. self.assertTrue(image['protected'])
  654. self.assertNotIn('type', image, response.text)
  655. # Try to download data before its uploaded
  656. path = self._url('/v2/images/%s/file' % image_id)
  657. headers = self._headers()
  658. response = requests.get(path, headers=headers)
  659. self.assertEqual(http.NO_CONTENT, response.status_code)
  660. # Upload some image data
  661. path = self._url('/v2/images/%s/file' % image_id)
  662. headers = self._headers({'Content-Type': 'application/octet-stream'})
  663. image_data = b'ZZZZZ'
  664. response = requests.put(path, headers=headers, data=image_data)
  665. self.assertEqual(http.NO_CONTENT, response.status_code)
  666. expect_c = six.text_type(hashlib.md5(image_data).hexdigest())
  667. expect_h = six.text_type(hashlib.sha512(image_data).hexdigest())
  668. func_utils.verify_image_hashes_and_status(self, image_id, expect_c,
  669. expect_h, 'active')
  670. # `disk_format` and `container_format` cannot
  671. # be replaced when the image is active.
  672. immutable_paths = ['/disk_format', '/container_format']
  673. media_type = 'application/openstack-images-v2.1-json-patch'
  674. headers = self._headers({'content-type': media_type})
  675. path = self._url('/v2/images/%s' % image_id)
  676. for immutable_path in immutable_paths:
  677. data = jsonutils.dumps([
  678. {'op': 'replace', 'path': immutable_path, 'value': 'ari'},
  679. ])
  680. response = requests.patch(path, headers=headers, data=data)
  681. self.assertEqual(http.FORBIDDEN, response.status_code)
  682. # Try to download the data that was just uploaded
  683. path = self._url('/v2/images/%s/file' % image_id)
  684. response = requests.get(path, headers=self._headers())
  685. self.assertEqual(http.OK, response.status_code)
  686. self.assertEqual(expect_c, response.headers['Content-MD5'])
  687. self.assertEqual('ZZZZZ', response.text)
  688. # Uploading duplicate data should be rejected with a 409. The
  689. # original data should remain untouched.
  690. path = self._url('/v2/images/%s/file' % image_id)
  691. headers = self._headers({'Content-Type': 'application/octet-stream'})
  692. response = requests.put(path, headers=headers, data='XXX')
  693. self.assertEqual(http.CONFLICT, response.status_code)
  694. func_utils.verify_image_hashes_and_status(self, image_id, expect_c,
  695. expect_h, 'active')
  696. # Ensure the size is updated to reflect the data uploaded
  697. path = self._url('/v2/images/%s' % image_id)
  698. response = requests.get(path, headers=self._headers())
  699. self.assertEqual(http.OK, response.status_code)
  700. self.assertEqual(5, jsonutils.loads(response.text)['size'])
  701. # Should be able to deactivate image
  702. path = self._url('/v2/images/%s/actions/deactivate' % image_id)
  703. response = requests.post(path, data={}, headers=self._headers())
  704. self.assertEqual(http.NO_CONTENT, response.status_code)
  705. # Change the image to public so TENANT2 can see it
  706. path = self._url('/v2/images/%s' % image_id)
  707. media_type = 'application/openstack-images-v2.0-json-patch'
  708. headers = self._headers({'content-type': media_type})
  709. data = jsonutils.dumps([{"replace": "/visibility", "value": "public"}])
  710. response = requests.patch(path, headers=headers, data=data)
  711. self.assertEqual(http.OK, response.status_code, response.text)
  712. # Tenant2 should get Forbidden when deactivating the public image
  713. path = self._url('/v2/images/%s/actions/deactivate' % image_id)
  714. response = requests.post(path, data={}, headers=self._headers(
  715. {'X-Tenant-Id': TENANT2}))
  716. self.assertEqual(http.FORBIDDEN, response.status_code)
  717. # Tenant2 should get Forbidden when reactivating the public image
  718. path = self._url('/v2/images/%s/actions/reactivate' % image_id)
  719. response = requests.post(path, data={}, headers=self._headers(
  720. {'X-Tenant-Id': TENANT2}))
  721. self.assertEqual(http.FORBIDDEN, response.status_code)
  722. # Deactivating a deactivated image succeeds (no-op)
  723. path = self._url('/v2/images/%s/actions/deactivate' % image_id)
  724. response = requests.post(path, data={}, headers=self._headers())
  725. self.assertEqual(http.NO_CONTENT, response.status_code)
  726. # Can't download a deactivated image
  727. path = self._url('/v2/images/%s/file' % image_id)
  728. response = requests.get(path, headers=self._headers())
  729. self.assertEqual(http.FORBIDDEN, response.status_code)
  730. # Deactivated image should still be in a listing
  731. path = self._url('/v2/images')
  732. response = requests.get(path, headers=self._headers())
  733. self.assertEqual(http.OK, response.status_code)
  734. images = jsonutils.loads(response.text)['images']
  735. self.assertEqual(2, len(images))
  736. self.assertEqual(image2_id, images[0]['id'])
  737. self.assertEqual(image_id, images[1]['id'])
  738. # Should be able to reactivate a deactivated image
  739. path = self._url('/v2/images/%s/actions/reactivate' % image_id)
  740. response = requests.post(path, data={}, headers=self._headers())
  741. self.assertEqual(http.NO_CONTENT, response.status_code)
  742. # Reactivating an active image succeeds (no-op)
  743. path = self._url('/v2/images/%s/actions/reactivate' % image_id)
  744. response = requests.post(path, data={}, headers=self._headers())
  745. self.assertEqual(http.NO_CONTENT, response.status_code)
  746. # Deletion should not work on protected images
  747. path = self._url('/v2/images/%s' % image_id)
  748. response = requests.delete(path, headers=self._headers())
  749. self.assertEqual(http.FORBIDDEN, response.status_code)
  750. # Unprotect image for deletion
  751. path = self._url('/v2/images/%s' % image_id)
  752. media_type = 'application/openstack-images-v2.1-json-patch'
  753. headers = self._headers({'content-type': media_type})
  754. doc = [{'op': 'replace', 'path': '/protected', 'value': False}]
  755. data = jsonutils.dumps(doc)
  756. response = requests.patch(path, headers=headers, data=data)
  757. self.assertEqual(http.OK, response.status_code, response.text)
  758. # Deletion should work. Deleting image-1
  759. path = self._url('/v2/images/%s' % image_id)
  760. response = requests.delete(path, headers=self._headers())
  761. self.assertEqual(http.NO_CONTENT, response.status_code)
  762. # This image should be no longer be directly accessible
  763. path = self._url('/v2/images/%s' % image_id)
  764. response = requests.get(path, headers=self._headers())
  765. self.assertEqual(http.NOT_FOUND, response.status_code)
  766. # And neither should its data
  767. path = self._url('/v2/images/%s/file' % image_id)
  768. headers = self._headers()
  769. response = requests.get(path, headers=headers)
  770. self.assertEqual(http.NOT_FOUND, response.status_code)
  771. # Image list should now contain just image-2
  772. path = self._url('/v2/images')
  773. response = requests.get(path, headers=self._headers())
  774. self.assertEqual(http.OK, response.status_code)
  775. images = jsonutils.loads(response.text)['images']
  776. self.assertEqual(1, len(images))
  777. self.assertEqual(image2_id, images[0]['id'])
  778. # Deleting image-2 should work
  779. path = self._url('/v2/images/%s' % image2_id)
  780. response = requests.delete(path, headers=self._headers())
  781. self.assertEqual(http.NO_CONTENT, response.status_code)
  782. # Image list should now be empty
  783. path = self._url('/v2/images')
  784. response = requests.get(path, headers=self._headers())
  785. self.assertEqual(http.OK, response.status_code)
  786. images = jsonutils.loads(response.text)['images']
  787. self.assertEqual(0, len(images))
  788. # Create image that tries to send True should return 400
  789. path = self._url('/v2/images')
  790. headers = self._headers({'content-type': 'application/json'})
  791. data = 'true'
  792. response = requests.post(path, headers=headers, data=data)
  793. self.assertEqual(http.BAD_REQUEST, response.status_code)
  794. # Create image that tries to send a string should return 400
  795. path = self._url('/v2/images')
  796. headers = self._headers({'content-type': 'application/json'})
  797. data = '"hello"'
  798. response = requests.post(path, headers=headers, data=data)
  799. self.assertEqual(http.BAD_REQUEST, response.status_code)
  800. # Create image that tries to send 123 should return 400
  801. path = self._url('/v2/images')
  802. headers = self._headers({'content-type': 'application/json'})
  803. data = '123'
  804. response = requests.post(path, headers=headers, data=data)
  805. self.assertEqual(http.BAD_REQUEST, response.status_code)
  806. self.stop_servers()
  807. def test_hidden_images(self):
  808. # Image list should be empty
  809. self.api_server.show_multiple_locations = True
  810. self.start_servers(**self.__dict__.copy())
  811. path = self._url('/v2/images')
  812. response = requests.get(path, headers=self._headers())
  813. self.assertEqual(http.OK, response.status_code)
  814. images = jsonutils.loads(response.text)['images']
  815. self.assertEqual(0, len(images))
  816. # Create an image
  817. path = self._url('/v2/images')
  818. headers = self._headers({'content-type': 'application/json'})
  819. data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel',
  820. 'disk_format': 'aki',
  821. 'container_format': 'aki',
  822. 'protected': False})
  823. response = requests.post(path, headers=headers, data=data)
  824. self.assertEqual(http.CREATED, response.status_code)
  825. # Returned image entity should have a generated id and status
  826. image = jsonutils.loads(response.text)
  827. image_id = image['id']
  828. checked_keys = set([
  829. u'status',
  830. u'name',
  831. u'tags',
  832. u'created_at',
  833. u'updated_at',
  834. u'visibility',
  835. u'self',
  836. u'protected',
  837. u'os_hidden',
  838. u'id',
  839. u'file',
  840. u'min_disk',
  841. u'type',
  842. u'min_ram',
  843. u'schema',
  844. u'disk_format',
  845. u'container_format',
  846. u'owner',
  847. u'checksum',
  848. u'os_hash_algo',
  849. u'os_hash_value',
  850. u'size',
  851. u'virtual_size',
  852. u'locations',
  853. ])
  854. self.assertEqual(checked_keys, set(image.keys()))
  855. # Returned image entity should have os_hidden as False
  856. expected_image = {
  857. 'status': 'queued',
  858. 'name': 'image-1',
  859. 'tags': [],
  860. 'visibility': 'shared',
  861. 'self': '/v2/images/%s' % image_id,
  862. 'protected': False,
  863. 'os_hidden': False,
  864. 'file': '/v2/images/%s/file' % image_id,
  865. 'min_disk': 0,
  866. 'type': 'kernel',
  867. 'min_ram': 0,
  868. 'schema': '/v2/schemas/image',
  869. }
  870. for key, value in expected_image.items():
  871. self.assertEqual(value, image[key], key)
  872. # Image list should now have one entry
  873. path = self._url('/v2/images')
  874. response = requests.get(path, headers=self._headers())
  875. self.assertEqual(http.OK, response.status_code)
  876. images = jsonutils.loads(response.text)['images']
  877. self.assertEqual(1, len(images))
  878. self.assertEqual(image_id, images[0]['id'])
  879. # Create another image wiht hidden true
  880. path = self._url('/v2/images')
  881. headers = self._headers({'content-type': 'application/json'})
  882. data = jsonutils.dumps({'name': 'image-2', 'type': 'kernel',
  883. 'disk_format': 'aki',
  884. 'container_format': 'aki',
  885. 'os_hidden': True})
  886. response = requests.post(path, headers=headers, data=data)
  887. self.assertEqual(http.CREATED, response.status_code)
  888. # Returned image entity should have a generated id and status
  889. image = jsonutils.loads(response.text)
  890. image2_id = image['id']
  891. checked_keys = set([
  892. u'status',
  893. u'name',
  894. u'tags',
  895. u'created_at',
  896. u'updated_at',
  897. u'visibility',
  898. u'self',
  899. u'protected',
  900. u'os_hidden',
  901. u'id',
  902. u'file',
  903. u'min_disk',
  904. u'type',
  905. u'min_ram',
  906. u'schema',
  907. u'disk_format',
  908. u'container_format',
  909. u'owner',
  910. u'checksum',
  911. u'os_hash_algo',
  912. u'os_hash_value',
  913. u'size',
  914. u'virtual_size',
  915. u'locations',
  916. ])
  917. self.assertEqual(checked_keys, set(image.keys()))
  918. # Returned image entity should have os_hidden as True
  919. expected_image = {
  920. 'status': 'queued',
  921. 'name': 'image-2',
  922. 'tags': [],
  923. 'visibility': 'shared',
  924. 'self': '/v2/images/%s' % image2_id,
  925. 'protected': False,
  926. 'os_hidden': True,
  927. 'file': '/v2/images/%s/file' % image2_id,
  928. 'min_disk': 0,
  929. 'type': 'kernel',
  930. 'min_ram': 0,
  931. 'schema': '/v2/schemas/image',
  932. }
  933. for key, value in expected_image.items():
  934. self.assertEqual(value, image[key], key)
  935. # Image list should now have one entries
  936. path = self._url('/v2/images')
  937. response = requests.get(path, headers=self._headers())
  938. self.assertEqual(http.OK, response.status_code)
  939. images = jsonutils.loads(response.text)['images']
  940. self.assertEqual(1, len(images))
  941. self.assertEqual(image_id, images[0]['id'])
  942. # Image list should list should show one image based on the filter
  943. # 'hidden=false'
  944. path = self._url('/v2/images?os_hidden=false')
  945. response = requests.get(path, headers=self._headers())
  946. self.assertEqual(http.OK, response.status_code)
  947. images = jsonutils.loads(response.text)['images']
  948. self.assertEqual(1, len(images))
  949. self.assertEqual(image_id, images[0]['id'])
  950. # Image list should list should show one image based on the filter
  951. # 'hidden=true'
  952. path = self._url('/v2/images?os_hidden=true')
  953. response = requests.get(path, headers=self._headers())
  954. self.assertEqual(http.OK, response.status_code)
  955. images = jsonutils.loads(response.text)['images']
  956. self.assertEqual(1, len(images))
  957. self.assertEqual(image2_id, images[0]['id'])
  958. # Image list should return 400 based on the filter
  959. # 'hidden=abcd'
  960. path = self._url('/v2/images?os_hidden=abcd')
  961. response = requests.get(path, headers=self._headers())
  962. self.assertEqual(http.BAD_REQUEST, response.status_code)
  963. # Upload some image data to image-1
  964. path = self._url('/v2/images/%s/file' % image_id)
  965. headers = self._headers({'Content-Type': 'application/octet-stream'})
  966. image_data = b'ZZZZZ'
  967. response = requests.put(path, headers=headers, data=image_data)
  968. self.assertEqual(http.NO_CONTENT, response.status_code)
  969. expect_c = six.text_type(hashlib.md5(image_data).hexdigest())
  970. expect_h = six.text_type(hashlib.sha512(image_data).hexdigest())
  971. func_utils.verify_image_hashes_and_status(self,
  972. image_id,
  973. expect_c,
  974. expect_h,
  975. status='active')
  976. # Upload some image data to image-2
  977. path = self._url('/v2/images/%s/file' % image2_id)
  978. headers = self._headers({'Content-Type': 'application/octet-stream'})
  979. image_data = b'WWWWW'
  980. response = requests.put(path, headers=headers, data=image_data)
  981. self.assertEqual(http.NO_CONTENT, response.status_code)
  982. expect_c = six.text_type(hashlib.md5(image_data).hexdigest())
  983. expect_h = six.text_type(hashlib.sha512(image_data).hexdigest())
  984. func_utils.verify_image_hashes_and_status(self,
  985. image2_id,
  986. expect_c,
  987. expect_h,
  988. status='active')
  989. # Hide image-1
  990. path = self._url('/v2/images/%s' % image_id)
  991. media_type = 'application/openstack-images-v2.1-json-patch'
  992. headers = self._headers({'content-type': media_type})
  993. data = jsonutils.dumps([
  994. {'op': 'replace', 'path': '/os_hidden', 'value': True},
  995. ])
  996. response = requests.patch(path, headers=headers, data=data)
  997. self.assertEqual(http.OK, response.status_code, response.text)
  998. # Returned image entity should reflect the changes
  999. image = jsonutils.loads(response.text)
  1000. self.assertTrue(image['os_hidden'])
  1001. # Image list should now have 0 entries
  1002. path = self._url('/v2/images')
  1003. response = requests.get(path, headers=self._headers())
  1004. self.assertEqual(http.OK, response.status_code)
  1005. images = jsonutils.loads(response.text)['images']
  1006. self.assertEqual(0, len(images))
  1007. # Image list should list should show image-1, and image-2 based
  1008. # on the filter 'hidden=true'
  1009. path = self._url('/v2/images?os_hidden=true')
  1010. response = requests.get(path, headers=self._headers())
  1011. self.assertEqual(http.OK, response.status_code)
  1012. images = jsonutils.loads(response.text)['images']
  1013. self.assertEqual(2, len(images))
  1014. self.assertEqual(image2_id, images[0]['id'])
  1015. self.assertEqual(image_id, images[1]['id'])
  1016. # Un-Hide image-1
  1017. path = self._url('/v2/images/%s' % image_id)
  1018. media_type = 'application/openstack-images-v2.1-json-patch'
  1019. headers = self._headers({'content-type': media_type})
  1020. data = jsonutils.dumps([
  1021. {'op': 'replace', 'path': '/os_hidden', 'value': False},
  1022. ])
  1023. response = requests.patch(path, headers=headers, data=data)
  1024. self.assertEqual(http.OK, response.status_code, response.text)
  1025. # Returned image entity should reflect the changes
  1026. image = jsonutils.loads(response.text)
  1027. self.assertFalse(image['os_hidden'])
  1028. # Image list should now have 1 entry
  1029. path = self._url('/v2/images')
  1030. response = requests.get(path, headers=self._headers())
  1031. self.assertEqual(http.OK, response.status_code)
  1032. images = jsonutils.loads(response.text)['images']
  1033. self.assertEqual(1, len(images))
  1034. self.assertEqual(image_id, images[0]['id'])
  1035. # Deleting image-1 should work
  1036. path = self._url('/v2/images/%s' % image_id)
  1037. response = requests.delete(path, headers=self._headers())
  1038. self.assertEqual(http.NO_CONTENT, response.status_code)
  1039. # Deleting image-2 should work
  1040. path = self._url('/v2/images/%s' % image2_id)
  1041. response = requests.delete(path, headers=self._headers())
  1042. self.assertEqual(http.NO_CONTENT, response.status_code)
  1043. # Image list should now be empty
  1044. path = self._url('/v2/images')
  1045. response = requests.get(path, headers=self._headers())
  1046. self.assertEqual(http.OK, response.status_code)
  1047. images = jsonutils.loads(response.text)['images']
  1048. self.assertEqual(0, len(images))
  1049. self.stop_servers()
  1050. def test_update_readonly_prop(self):
  1051. self.start_servers(**self.__dict__.copy())
  1052. # Create an image (with two deployer-defined properties)
  1053. path = self._url('/v2/images')
  1054. headers = self._headers({'content-type': 'application/json'})
  1055. data = jsonutils.dumps({'name': 'image-1'})
  1056. response = requests.post(path, headers=headers, data=data)
  1057. image = jsonutils.loads(response.text)
  1058. image_id = image['id']
  1059. path = self._url('/v2/images/%s' % image_id)
  1060. media_type = 'application/openstack-images-v2.1-json-patch'
  1061. headers = self._headers({'content-type': media_type})
  1062. props = ['/id', '/file', '/location', '/schema', '/self']
  1063. for prop in props:
  1064. doc = [{'op': 'replace',
  1065. 'path': prop,
  1066. 'value': 'value1'}]
  1067. data = jsonutils.dumps(doc)
  1068. response = requests.patch(path, headers=headers, data=data)
  1069. self.assertEqual(http.FORBIDDEN, response.status_code)
  1070. for prop in props:
  1071. doc = [{'op': 'remove',
  1072. 'path': prop,
  1073. 'value': 'value1'}]
  1074. data = jsonutils.dumps(doc)
  1075. response = requests.patch(path, headers=headers, data=data)
  1076. self.assertEqual(http.FORBIDDEN, response.status_code)
  1077. for prop in props:
  1078. doc = [{'op': 'add',
  1079. 'path': prop,
  1080. 'value': 'value1'}]
  1081. data = jsonutils.dumps(doc)
  1082. response = requests.patch(path, headers=headers, data=data)
  1083. self.assertEqual(http.FORBIDDEN, response.status_code)
  1084. self.stop_servers()
  1085. def test_methods_that_dont_accept_illegal_bodies(self):
  1086. # Check images can be reached
  1087. self.start_servers(**self.__dict__.copy())
  1088. path = self._url('/v2/images')
  1089. response = requests.get(path, headers=self._headers())
  1090. self.assertEqual(http.OK, response.status_code)
  1091. # Test all the schemas
  1092. schema_urls = [
  1093. '/v2/schemas/images',
  1094. '/v2/schemas/image',
  1095. '/v2/schemas/members',
  1096. '/v2/schemas/member',
  1097. ]
  1098. for value in schema_urls:
  1099. path = self._url(value)
  1100. data = jsonutils.dumps(["body"])
  1101. response = requests.get(path, headers=self._headers(), data=data)
  1102. self.assertEqual(http.BAD_REQUEST, response.status_code)
  1103. # Create image for use with tests
  1104. path = self._url('/v2/images')
  1105. headers = self._headers({'content-type': 'application/json'})
  1106. data = jsonutils.dumps({'name': 'image'})
  1107. response = requests.post(path, headers=headers, data=data)
  1108. self.assertEqual(http.CREATED, response.status_code)
  1109. image = jsonutils.loads(response.text)
  1110. image_id = image['id']
  1111. test_urls = [
  1112. ('/v2/images/%s', 'get'),
  1113. ('/v2/images/%s/actions/deactivate', 'post'),
  1114. ('/v2/images/%s/actions/reactivate', 'post'),
  1115. ('/v2/images/%s/tags/mytag', 'put'),
  1116. ('/v2/images/%s/tags/mytag', 'delete'),
  1117. ('/v2/images/%s/members', 'get'),
  1118. ('/v2/images/%s/file', 'get'),
  1119. ('/v2/images/%s', 'delete'),
  1120. ]
  1121. for link, method in test_urls:
  1122. path = self._url(link % image_id)
  1123. data = jsonutils.dumps(["body"])
  1124. response = getattr(requests, method)(
  1125. path, headers=self._headers(), data=data)
  1126. self.assertEqual(http.BAD_REQUEST, response.status_code)
  1127. # DELETE /images/imgid without legal json
  1128. path = self._url('/v2/images/%s' % image_id)
  1129. data = '{"hello"]'
  1130. response = requests.delete(path, headers=self._headers(), data=data)
  1131. self.assertEqual(http.BAD_REQUEST, response.status_code)
  1132. # POST /images/imgid/members
  1133. path = self._url('/v2/images/%s/members' % image_id)
  1134. data = jsonutils.dumps({'member': TENANT3})
  1135. response = requests.post(path, headers=self._headers(), data=data)
  1136. self.assertEqual(http.OK, response.status_code)
  1137. # GET /images/imgid/members/memid
  1138. path = self._url('/v2/images/%s/members/%s' % (image_id, TENANT3))
  1139. data = jsonutils.dumps(["body"])
  1140. response = requests.get(path, headers=self._headers(), data=data)
  1141. self.assertEqual(http.BAD_REQUEST, response.status_code)
  1142. # DELETE /images/imgid/members/memid
  1143. path = self._url('/v2/images/%s/members/%s' % (image_id, TENANT3))
  1144. data = jsonutils.dumps(["body"])
  1145. response = requests.delete(path, headers=self._headers(), data=data)
  1146. self.assertEqual(http.BAD_REQUEST, response.status_code)
  1147. self.stop_servers()
  1148. def test_download_random_access_w_range_request(self):
  1149. """
  1150. Test partial download 'Range' requests for images (random image access)
  1151. """
  1152. self.start_servers(**self.__dict__.copy())
  1153. # Create an image (with two deployer-defined properties)
  1154. path = self._url('/v2/images')
  1155. headers = self._headers({'content-type': 'application/json'})
  1156. data = jsonutils.dumps({'name': 'image-2', 'type': 'kernel',
  1157. 'bar': 'foo', 'disk_format': 'aki',
  1158. 'container_format': 'aki', 'xyz': 'abc'})
  1159. response = requests.post(path, headers=headers, data=data)
  1160. self.assertEqual(http.CREATED, response.status_code)
  1161. image = jsonutils.loads(response.text)
  1162. image_id = image['id']
  1163. # Upload data to image
  1164. image_data = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  1165. path = self._url('/v2/images/%s/file' % image_id)
  1166. headers = self._headers({'Content-Type': 'application/octet-stream'})
  1167. response = requests.put(path, headers=headers, data=image_data)
  1168. self.assertEqual(http.NO_CONTENT, response.status_code)
  1169. # test for success on satisfiable Range request.
  1170. range_ = 'bytes=3-10'
  1171. headers = self._headers({'Range': range_})
  1172. path = self._url('/v2/images/%s/file' % image_id)
  1173. response = requests.get(path, headers=headers)
  1174. self.assertEqual(http.PARTIAL_CONTENT, response.status_code)
  1175. self.assertEqual('DEFGHIJK', response.text)
  1176. # test for failure on unsatisfiable Range request.
  1177. range_ = 'bytes=10-5'
  1178. headers = self._headers({'Range': range_})
  1179. path = self._url('/v2/images/%s/file' % image_id)
  1180. response = requests.get(path, headers=headers)
  1181. self.assertEqual(http.REQUESTED_RANGE_NOT_SATISFIABLE,
  1182. response.status_code)
  1183. self.stop_servers()
  1184. def test_download_random_access_w_content_range(self):
  1185. """
  1186. Even though Content-Range is incorrect on requests, we support it
  1187. for backward compatibility with clients written for pre-Pike Glance.
  1188. The following test is for 'Content-Range' requests, which we have
  1189. to ensure that we prevent regression.
  1190. """
  1191. self.start_servers(**self.__dict__.copy())
  1192. # Create another image (with two deployer-defined properties)
  1193. path = self._url('/v2/images')
  1194. headers = self._headers({'content-type': 'application/json'})
  1195. data = jsonutils.dumps({'name': 'image-2', 'type': 'kernel',
  1196. 'bar': 'foo', 'disk_format': 'aki',
  1197. 'container_format': 'aki', 'xyz': 'abc'})
  1198. response = requests.post(path, headers=headers, data=data)
  1199. self.assertEqual(http.CREATED, response.status_code)
  1200. image = jsonutils.loads(response.text)
  1201. image_id = image['id']
  1202. # Upload data to image
  1203. image_data = 'Z' * 15
  1204. path = self._url('/v2/images/%s/file' % image_id)
  1205. headers = self._headers({'Content-Type': 'application/octet-stream'})
  1206. response = requests.put(path, headers=headers, data=image_data)
  1207. self.assertEqual(http.NO_CONTENT, response.status_code)
  1208. result_body = ''
  1209. for x in range(15):
  1210. # NOTE(flaper87): Read just 1 byte. Content-Range is
  1211. # 0-indexed and it specifies the first byte to read
  1212. # and the last byte to read.
  1213. content_range = 'bytes %s-%s/15' % (x, x)
  1214. headers = self._headers({'Content-Range': content_range})
  1215. path = self._url('/v2/images/%s/file' % image_id)
  1216. response = requests.get(path, headers=headers)
  1217. self.assertEqual(http.PARTIAL_CONTENT, response.status_code)
  1218. result_body += response.text
  1219. self.assertEqual(result_body, image_data)
  1220. # test for failure on unsatisfiable request for ContentRange.
  1221. content_range = 'bytes 3-16/15'
  1222. headers = self._headers({'Content-Range': content_range})
  1223. path = self._url('/v2/images/%s/file' % image_id)
  1224. response = requests.get(path, headers=headers)
  1225. self.assertEqual(http.REQUESTED_RANGE_NOT_SATISFIABLE,
  1226. response.status_code)
  1227. self.stop_servers()
  1228. def test_download_policy_when_cache_is_not_enabled(self):
  1229. rules = {'context_is_admin': 'role:admin',
  1230. 'default': '',
  1231. 'add_image': '',
  1232. 'get_image': '',
  1233. 'modify_image': '',
  1234. 'upload_image': '',
  1235. 'delete_image': '',
  1236. 'download_image': '!'}
  1237. self.set_policy_rules(rules)
  1238. self.start_servers(**self.__dict__.copy())
  1239. # Create an image
  1240. path = self._url('/v2/images')
  1241. headers = self._headers({'content-type': 'application/json',
  1242. 'X-Roles': 'member'})
  1243. data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki',
  1244. 'container_format': 'aki'})
  1245. response = requests.post(path, headers=headers, data=data)
  1246. self.assertEqual(http.CREATED, response.status_code)
  1247. # Returned image entity
  1248. image = jsonutils.loads(response.text)
  1249. image_id = image['id']
  1250. expected_image = {
  1251. 'status': 'queued',
  1252. 'name': 'image-1',
  1253. 'tags': [],
  1254. 'visibility': 'shared',
  1255. 'self': '/v2/images/%s' % image_id,
  1256. 'protected': False,
  1257. 'file': '/v2/images/%s/file' % image_id,
  1258. 'min_disk': 0,
  1259. 'min_ram': 0,
  1260. 'schema': '/v2/schemas/image',
  1261. }
  1262. for key, value in six.iteritems(expected_image):
  1263. self.assertEqual(value, image[key], key)
  1264. # Upload data to image
  1265. path = self._url('/v2/images/%s/file' % image_id)
  1266. headers = self._headers({'Content-Type': 'application/octet-stream'})
  1267. response = requests.put(path, headers=headers, data='ZZZZZ')
  1268. self.assertEqual(http.NO_CONTENT, response.status_code)
  1269. # Get an image should fail
  1270. path = self._url('/v2/images/%s/file' % image_id)
  1271. headers = self._headers({'Content-Type': 'application/octet-stream'})
  1272. response = requests.get(path, headers=headers)
  1273. self.assertEqual(http.FORBIDDEN, response.status_code)
  1274. # Image Deletion should work
  1275. path = self._url('/v2/images/%s' % image_id)
  1276. response = requests.delete(path, headers=self._headers())
  1277. self.assertEqual(http.NO_CONTENT, response.status_code)
  1278. # This image should be no longer be directly accessible
  1279. path = self._url('/v2/images/%s' % image_id)
  1280. response = requests.get(path, headers=self._headers())
  1281. self.assertEqual(http.NOT_FOUND, response.status_code)
  1282. self.stop_servers()
  1283. def test_download_image_not_allowed_using_restricted_policy(self):
  1284. rules = {
  1285. "context_is_admin": "role:admin",
  1286. "default": "",
  1287. "add_image": "",
  1288. "get_image": "",
  1289. "modify_image": "",
  1290. "upload_image": "",
  1291. "delete_image": "",
  1292. "restricted":
  1293. "not ('aki':%(container_format)s and role:_member_)",
  1294. "download_image": "role:admin or rule:restricted"
  1295. }
  1296. self.set_policy_rules(rules)
  1297. self.start_servers(**self.__dict__.copy())
  1298. # Create an image
  1299. path = self._url('/v2/images')
  1300. headers = self._headers({'content-type': 'application/json',
  1301. 'X-Roles': 'member'})
  1302. data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki',
  1303. 'container_format': 'aki'})
  1304. response = requests.post(path, headers=headers, data=data)
  1305. self.assertEqual(http.CREATED, response.status_code)
  1306. # Returned image entity
  1307. image = jsonutils.loads(response.text)
  1308. image_id = image['id']
  1309. expected_image = {
  1310. 'status': 'queued',
  1311. 'name': 'image-1',
  1312. 'tags': [],
  1313. 'visibility': 'shared',
  1314. 'self': '/v2/images/%s' % image_id,
  1315. 'protected': False,
  1316. 'file': '/v2/images/%s/file' % image_id,
  1317. 'min_disk': 0,
  1318. 'min_ram': 0,
  1319. 'schema': '/v2/schemas/image',
  1320. }
  1321. for key, value in six.iteritems(expected_image):
  1322. self.assertEqual(value, image[key], key)
  1323. # Upload data to image
  1324. path = self._url('/v2/images/%s/file' % image_id)
  1325. headers = self._headers({'Content-Type': 'application/octet-stream'})
  1326. response = requests.put(path, headers=headers, data='ZZZZZ')
  1327. self.assertEqual(http.NO_CONTENT, response.status_code)
  1328. # Get an image should fail
  1329. path = self._url('/v2/images/%s/file' % image_id)
  1330. headers = self._headers({'Content-Type': 'application/octet-stream',
  1331. 'X-Roles': '_member_'})
  1332. response = requests.get(path, headers=headers)
  1333. self.assertEqual(http.FORBIDDEN, response.status_code)
  1334. # Image Deletion should work
  1335. path = self._url('/v2/images/%s' % image_id)
  1336. response = requests.delete(path, headers=self._headers())
  1337. self.assertEqual(http.NO_CONTENT, response.status_code)
  1338. # This image should be no longer be directly accessible
  1339. path = self._url('/v2/images/%s' % image_id)
  1340. response = requests.get(path, headers=self._headers())
  1341. self.assertEqual(http.NOT_FOUND, response.status_code)
  1342. self.stop_servers()
  1343. def test_download_image_allowed_using_restricted_policy(self):
  1344. rules = {
  1345. "context_is_admin": "role:admin",
  1346. "default": "",
  1347. "add_image": "",
  1348. "get_image": "",
  1349. "modify_image": "",
  1350. "upload_image": "",
  1351. "get_image_location": "",
  1352. "delete_image": "",
  1353. "restricted":
  1354. "not ('aki':%(container_format)s and role:_member_)",
  1355. "download_image": "role:admin or rule:restricted"
  1356. }
  1357. self.set_policy_rules(rules)
  1358. self.start_servers(**self.__dict__.copy())
  1359. # Create an image
  1360. path = self._url('/v2/images')
  1361. headers = self._headers({'content-type': 'application/json',
  1362. 'X-Roles': 'member'})
  1363. data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki',
  1364. 'container_format': 'aki'})
  1365. response = requests.post(path, headers=headers, data=data)
  1366. self.assertEqual(http.CREATED, response.status_code)
  1367. # Returned image entity
  1368. image = jsonutils.loads(response.text)
  1369. image_id = image['id']
  1370. expected_image = {
  1371. 'status': 'queued',
  1372. 'name': 'image-1',
  1373. 'tags': [],
  1374. 'visibility': 'shared',
  1375. 'self': '/v2/images/%s' % image_id,
  1376. 'protected': False,
  1377. 'file': '/v2/images/%s/file' % image_id,
  1378. 'min_disk': 0,
  1379. 'min_ram': 0,
  1380. 'schema': '/v2/schemas/image',
  1381. }
  1382. for key, value in six.iteritems(expected_image):
  1383. self.assertEqual(value, image[key], key)
  1384. # Upload data to image
  1385. path = self._url('/v2/images/%s/file' % image_id)
  1386. headers = self._headers({'Content-Type': 'application/octet-stream'})
  1387. response = requests.put(path, headers=headers, data='ZZZZZ')
  1388. self.assertEqual(http.NO_CONTENT, response.status_code)
  1389. # Get an image should be allowed
  1390. path = self._url('/v2/images/%s/file' % image_id)
  1391. headers = self._headers({'Content-Type': 'application/octet-stream',
  1392. 'X-Roles': 'member'})
  1393. response = requests.get(path, headers=headers)
  1394. self.assertEqual(http.OK, response.status_code)
  1395. # Image Deletion should work
  1396. path = self._url('/v2/images/%s' % image_id)
  1397. response = requests.delete(path, headers=self._headers())
  1398. self.assertEqual(http.NO_CONTENT, response.status_code)
  1399. # This image should be no longer be directly accessible
  1400. path = self._url('/v2/images/%s' % image_id)
  1401. response = requests.get(path, headers=self._headers())
  1402. self.assertEqual(http.NOT_FOUND, response.status_code)
  1403. self.stop_servers()
  1404. def test_download_image_raises_service_unavailable(self):
  1405. """Test image download returns HTTPServiceUnavailable."""
  1406. self.api_server.show_multiple_locations = True
  1407. self.start_servers(**self.__dict__.copy())
  1408. # Create an image
  1409. path = self._url('/v2/images')
  1410. headers = self._headers({'content-type': 'application/json'})
  1411. data = jsonutils.dumps({'name': 'image-1',
  1412. 'disk_format': 'aki',
  1413. 'container_format': 'aki'})
  1414. response = requests.post(path, headers=headers, data=data)
  1415. self.assertEqual(http.CREATED, response.status_code)
  1416. # Get image id
  1417. image = jsonutils.loads(response.text)
  1418. image_id = image['id']
  1419. # Update image locations via PATCH
  1420. path = self._url('/v2/images/%s' % image_id)
  1421. media_type = 'application/openstack-images-v2.1-json-patch'
  1422. headers = self._headers({'content-type': media_type})
  1423. http_server_pid, http_port = test_utils.start_http_server(image_id,
  1424. "image-1")
  1425. values = [{'url': 'http://127.0.0.1:%s/image-1' % http_port,
  1426. 'metadata': {'idx': '0'}}]
  1427. doc = [{'op': 'replace',
  1428. 'path': '/locations',
  1429. 'value': values}]
  1430. data = jsonutils.dumps(doc)
  1431. response = requests.patch(path, headers=headers, data=data)
  1432. self.assertEqual(http.OK, response.status_code)
  1433. # Download an image should work
  1434. path = self._url('/v2/images/%s/file' % image_id)
  1435. headers = self._headers({'Content-Type': 'application/json'})
  1436. response = requests.get(path, headers=headers)
  1437. self.assertEqual(http.OK, response.status_code)
  1438. # Stop http server used to update image location
  1439. os.kill(http_server_pid, signal.SIGKILL)
  1440. # Download an image should raise HTTPServiceUnavailable
  1441. path = self._url('/v2/images/%s/file' % image_id)
  1442. headers = self._headers({'Content-Type': 'application/json'})
  1443. response = requests.get(path, headers=headers)
  1444. self.assertEqual(http.SERVICE_UNAVAILABLE, response.status_code)
  1445. # Image Deletion should work
  1446. path = self._url('/v2/images/%s' % image_id)
  1447. response = requests.delete(path, headers=self._headers())
  1448. self.assertEqual(http.NO_CONTENT, response.status_code)
  1449. # This image should be no longer be directly accessible
  1450. path = self._url('/v2/images/%s' % image_id)
  1451. response = requests.get(path, headers=self._headers())
  1452. self.assertEqual(http.NOT_FOUND, response.status_code)
  1453. self.stop_servers()
  1454. def test_image_modification_works_for_owning_tenant_id(self):
  1455. rules = {
  1456. "context_is_admin": "role:admin",
  1457. "default": "",
  1458. "add_image": "",
  1459. "get_image": "",
  1460. "modify_image": "tenant:%(owner)s",
  1461. "upload_image": "",
  1462. "get_image_location": "",
  1463. "delete_image": "",
  1464. "restricted":
  1465. "not ('aki':%(container_format)s and role:_member_)",
  1466. "download_image": "role:admin or rule:restricted"
  1467. }
  1468. self.set_policy_rules(rules)
  1469. self.start_servers(**self.__dict__.copy())
  1470. path = self._url('/v2/images')
  1471. headers = self._headers({'content-type': 'application/json',
  1472. 'X-Roles': 'admin'})
  1473. data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki',
  1474. 'container_format': 'aki'})
  1475. response = requests.post(path, headers=headers, data=data)
  1476. self.assertEqual(http.CREATED, response.status_code)
  1477. # Get the image's ID
  1478. image = jsonutils.loads(response.text)
  1479. image_id = image['id']
  1480. path = self._url('/v2/images/%s' % image_id)
  1481. media_type = 'application/openstack-images-v2.1-json-patch'
  1482. headers['content-type'] = media_type
  1483. del headers['X-Roles']
  1484. data = jsonutils.dumps([
  1485. {'op': 'replace', 'path': '/name', 'value': 'new-name'},
  1486. ])
  1487. response = requests.patch(path, headers=headers, data=data)
  1488. self.assertEqual(http.OK, response.status_code)
  1489. self.stop_servers()
  1490. def test_image_modification_fails_on_mismatched_tenant_ids(self):
  1491. rules = {
  1492. "context_is_admin": "role:admin",
  1493. "default": "",
  1494. "add_image": "",
  1495. "get_image": "",
  1496. "modify_image": "'A-Fake-Tenant-Id':%(owner)s",
  1497. "upload_image": "",
  1498. "get_image_location": "",
  1499. "delete_image": "",
  1500. "restricted":
  1501. "not ('aki':%(container_format)s and role:_member_)",
  1502. "download_image": "role:admin or rule:restricted"
  1503. }
  1504. self.set_policy_rules(rules)
  1505. self.start_servers(**self.__dict__.copy())
  1506. path = self._url('/v2/images')
  1507. headers = self._headers({'content-type': 'application/json',
  1508. 'X-Roles': 'admin'})
  1509. data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki',
  1510. 'container_format': 'aki'})
  1511. response = requests.post(path, headers=headers, data=data)
  1512. self.assertEqual(http.CREATED, response.status_code)
  1513. # Get the image's ID
  1514. image = jsonutils.loads(response.text)
  1515. image_id = image['id']
  1516. path = self._url('/v2/images/%s' % image_id)
  1517. media_type = 'application/openstack-images-v2.1-json-patch'
  1518. headers['content-type'] = media_type
  1519. del headers['X-Roles']
  1520. data = jsonutils.dumps([
  1521. {'op': 'replace', 'path': '/name', 'value': 'new-name'},
  1522. ])
  1523. response = requests.patch(path, headers=headers, data=data)
  1524. self.assertEqual(http.FORBIDDEN, response.status_code)
  1525. self.stop_servers()
  1526. def test_member_additions_works_for_owning_tenant_id(self):
  1527. rules = {
  1528. "context_is_admin": "role:admin",
  1529. "default": "",
  1530. "add_image": "",
  1531. "get_image": "",
  1532. "modify_image": "",
  1533. "upload_image": "",
  1534. "get_image_location": "",
  1535. "delete_image": "",
  1536. "restricted":
  1537. "not ('aki':%(container_format)s and role:_member_)",
  1538. "download_image": "role:admin or rule:restricted",
  1539. "add_member": "tenant:%(owner)s",
  1540. }
  1541. self.set_policy_rules(rules)
  1542. self.start_servers(**self.__dict__.copy())
  1543. path = self._url('/v2/images')
  1544. headers = self._headers({'content-type': 'application/json',
  1545. 'X-Roles': 'admin'})
  1546. data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki',
  1547. 'container_format': 'aki'})
  1548. response = requests.post(path, headers=headers, data=data)
  1549. self.assertEqual(http.CREATED, response.status_code)
  1550. # Get the image's ID
  1551. image = jsonutils.loads(response.text)
  1552. image_id = image['id']
  1553. # Get the image's members resource
  1554. path = self._url('/v2/images/%s/members' % image_id)
  1555. body = jsonutils.dumps({'member': TENANT3})
  1556. del headers['X-Roles']
  1557. response = requests.post(path, headers=headers, data=body)
  1558. self.assertEqual(http.OK, response.status_code)
  1559. self.stop_servers()
  1560. def test_image_additions_works_only_for_specific_tenant_id(self):
  1561. rules = {
  1562. "context_is_admin": "role:admin",
  1563. "default": "",
  1564. "add_image": "'{0}':%(owner)s".format(TENANT1),
  1565. "get_image": "",
  1566. "modify_image": "",
  1567. "upload_image": "",
  1568. "get_image_location": "",
  1569. "delete_image": "",
  1570. "restricted":
  1571. "not ('aki':%(container_format)s and role:_member_)",
  1572. "download_image": "role:admin or rule:restricted",
  1573. "add_member": "",
  1574. }
  1575. self.set_policy_rules(rules)
  1576. self.start_servers(**self.__dict__.copy())
  1577. path = self._url('/v2/images')
  1578. headers = self._headers({'content-type': 'application/json',
  1579. 'X-Roles': 'admin', 'X-Tenant-Id': TENANT1})
  1580. data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki',
  1581. 'container_format': 'aki'})
  1582. response = requests.post(path, headers=headers, data=data)
  1583. self.assertEqual(http.CREATED, response.status_code)
  1584. headers['X-Tenant-Id'] = TENANT2
  1585. response = requests.post(path, headers=headers, data=data)
  1586. self.assertEqual(http.FORBIDDEN, response.status_code)
  1587. self.stop_servers()
  1588. def test_owning_tenant_id_can_retrieve_image_information(self):
  1589. rules = {
  1590. "context_is_admin": "role:admin",
  1591. "default": "",
  1592. "add_image": "",
  1593. "get_image": "tenant:%(owner)s",
  1594. "modify_image": "",
  1595. "upload_image": "",
  1596. "get_image_location": "",
  1597. "delete_image": "",
  1598. "restricted":
  1599. "not ('aki':%(container_format)s and role:_member_)",
  1600. "download_image": "role:admin or rule:restricted",
  1601. "add_member": "",
  1602. }
  1603. self.set_policy_rules(rules)
  1604. self.start_servers(**self.__dict__.copy())
  1605. path = self._url('/v2/images')
  1606. headers = self._headers({'content-type': 'application/json',
  1607. 'X-Roles': 'admin', 'X-Tenant-Id': TENANT1})
  1608. data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki',
  1609. 'container_format': 'aki'})
  1610. response = requests.post(path, headers=headers, data=data)
  1611. self.assertEqual(http.CREATED, response.status_code)
  1612. # Remove the admin role
  1613. del headers['X-Roles']
  1614. # Get the image's ID
  1615. image = jsonutils.loads(response.text)
  1616. image_id = image['id']
  1617. # Can retrieve the image as TENANT1
  1618. path = self._url('/v2/images/%s' % image_id)
  1619. response = requests.get(path, headers=headers)
  1620. self.assertEqual(http.OK, response.status_code)
  1621. # Can retrieve the image's members as TENANT1
  1622. path = self._url('/v2/images/%s/members' % image_id)
  1623. response = requests.get(path, headers=headers)
  1624. self.assertEqual(http.OK, response.status_code)
  1625. headers['X-Tenant-Id'] = TENANT2
  1626. response = requests.get(path, headers=headers)
  1627. self.assertEqual(http.FORBIDDEN, response.status_code)
  1628. self.stop_servers()
  1629. def test_owning_tenant_can_publicize_image(self):
  1630. rules = {
  1631. "context_is_admin": "role:admin",
  1632. "default": "",
  1633. "add_image": "",
  1634. "publicize_image": "tenant:%(owner)s",
  1635. "get_image": "tenant:%(owner)s",
  1636. "modify_image": "",
  1637. "upload_image": "",
  1638. "get_image_location": "",
  1639. "delete_image": "",
  1640. "restricted":
  1641. "not ('aki':%(container_format)s and role:_member_)",
  1642. "download_image": "role:admin or rule:restricted",
  1643. "add_member": "",
  1644. }
  1645. self.set_policy_rules(rules)
  1646. self.start_servers(**self.__dict__.copy())
  1647. path = self._url('/v2/images')
  1648. headers = self._headers({'content-type': 'application/json',
  1649. 'X-Roles': 'admin', 'X-Tenant-Id': TENANT1})
  1650. data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki',
  1651. 'container_format': 'aki'})
  1652. response = requests.post(path, headers=headers, data=data)
  1653. self.assertEqual(http.CREATED, response.status_code)
  1654. # Get the image's ID
  1655. image = jsonutils.loads(response.text)
  1656. image_id = image['id']
  1657. path = self._url('/v2/images/%s' % image_id)
  1658. headers = self._headers({
  1659. 'Content-Type': 'application/openstack-images-v2.1-json-patch',
  1660. 'X-Tenant-Id': TENANT1,
  1661. })
  1662. doc = [{'op': 'replace', 'path': '/visibility', 'value': 'public'}]
  1663. data = jsonutils.dumps(doc)
  1664. response = requests.patch(path, headers=headers, data=data)
  1665. self.assertEqual(http.OK, response.status_code)
  1666. def test_owning_tenant_can_communitize_image(self):
  1667. rules = {
  1668. "context_is_admin": "role:admin",
  1669. "default": "",
  1670. "add_image": "",
  1671. "communitize_image": "tenant:%(owner)s",
  1672. "get_image": "tenant:%(owner)s",
  1673. "modify_image": "",
  1674. "upload_image": "",
  1675. "get_image_location": "",
  1676. "delete_image": "",
  1677. "restricted":
  1678. "not ('aki':%(container_format)s and role:_member_)",
  1679. "download_image": "role:admin or rule:restricted",
  1680. "add_member": "",
  1681. }
  1682. self.set_policy_rules(rules)
  1683. self.start_servers(**self.__dict__.copy())
  1684. path = self._url('/v2/images')
  1685. headers = self._headers({'content-type': 'application/json',
  1686. 'X-Roles': 'admin', 'X-Tenant-Id': TENANT1})
  1687. data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki',
  1688. 'container_format': 'aki'})
  1689. response = requests.post(path, headers=headers, data=data)
  1690. self.assertEqual(201, response.status_code)
  1691. # Get the image's ID
  1692. image = jsonutils.loads(response.text)
  1693. image_id = image['id']
  1694. path = self._url('/v2/images/%s' % image_id)
  1695. headers = self._headers({
  1696. 'Content-Type': 'application/openstack-images-v2.1-json-patch',
  1697. 'X-Tenant-Id': TENANT1,
  1698. })
  1699. doc = [{'op': 'replace', 'path': '/visibility', 'value': 'community'}]
  1700. data = jsonutils.dumps(doc)
  1701. response = requests.patch(path, headers=headers, data=data)
  1702. self.assertEqual(200, response.status_code)
  1703. def test_owning_tenant_can_delete_image(self):
  1704. rules = {
  1705. "context_is_admin": "role:admin",
  1706. "default": "",
  1707. "add_image": "",
  1708. "publicize_image": "tenant:%(owner)s",
  1709. "get_image": "tenant:%(owner)s",
  1710. "modify_image": "",
  1711. "upload_image": "",
  1712. "get_image_location": "",
  1713. "delete_image": "",
  1714. "restricted":
  1715. "not ('aki':%(container_format)s and role:_member_)",
  1716. "download_image": "role:admin or rule:restricted",
  1717. "add_member": "",
  1718. }
  1719. self.set_policy_rules(rules)
  1720. self.start_servers(**self.__dict__.copy())
  1721. path = self._url('/v2/images')
  1722. headers = self._headers({'content-type': 'application/json',
  1723. 'X-Roles': 'admin', 'X-Tenant-Id': TENANT1})
  1724. data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki',
  1725. 'container_format': 'aki'})
  1726. response = requests.post(path, headers=headers, data=data)
  1727. self.assertEqual(http.CREATED, response.status_code)
  1728. # Get the image's ID
  1729. image = jsonutils.loads(response.text)
  1730. image_id = image['id']
  1731. path = self._url('/v2/images/%s' % image_id)
  1732. response = requests.delete(path, headers=headers)
  1733. self.assertEqual(http.NO_CONTENT, response.status_code)
  1734. def test_list_show_ok_when_get_location_allowed_for_admins(self):
  1735. self.api_server.show_image_direct_url = True
  1736. self.api_server.show_multiple_locations = True
  1737. # setup context to allow a list locations by admin only
  1738. rules = {
  1739. "context_is_admin": "role:admin",
  1740. "default": "",
  1741. "add_image": "",
  1742. "get_image": "",
  1743. "modify_image": "",
  1744. "upload_image": "",
  1745. "get_image_location": "role:admin",
  1746. "delete_image": "",
  1747. "restricted": "",
  1748. "download_image": "",
  1749. "add_member": "",
  1750. }
  1751. self.set_policy_rules(rules)
  1752. self.start_servers(**self.__dict__.copy())
  1753. # Create an image
  1754. path = self._url('/v2/images')
  1755. headers = self._headers({'content-type': 'application/json',
  1756. 'X-Tenant-Id': TENANT1})
  1757. data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki',
  1758. 'container_format': 'aki'})
  1759. response = requests.post(path, headers=headers, data=data)
  1760. self.assertEqual(http.CREATED, response.status_code)
  1761. # Get the image's ID
  1762. image = jsonutils.loads(response.text)
  1763. image_id = image['id']
  1764. # Can retrieve the image as TENANT1
  1765. path = self._url('/v2/images/%s' % image_id)
  1766. response = requests.get(path, headers=headers)
  1767. self.assertEqual(http.OK, response.status_code)
  1768. # Can list images as TENANT1
  1769. path = self._url('/v2/images')
  1770. response = requests.get(path, headers=headers)
  1771. self.assertEqual(http.OK, response.status_code)
  1772. self.stop_servers()
  1773. def test_image_size_cap(self):
  1774. self.api_server.image_size_cap = 128
  1775. self.start_servers(**self.__dict__.copy())
  1776. # create an image
  1777. path = self._url('/v2/images')
  1778. headers = self._headers({'content-type': 'application/json'})
  1779. data = jsonutils.dumps({'name': 'image-size-cap-test-image',
  1780. 'type': 'kernel', 'disk_format': 'aki',
  1781. 'container_format': 'aki'})
  1782. response = requests.post(path, headers=headers, data=data)
  1783. self.assertEqual(http.CREATED, response.status_code)
  1784. image = jsonutils.loads(response.text)
  1785. image_id = image['id']
  1786. # try to populate it with oversized data
  1787. path = self._url('/v2/images/%s/file' % image_id)
  1788. headers = self._headers({'Content-Type': 'application/octet-stream'})
  1789. class StreamSim(object):
  1790. # Using a one-shot iterator to force chunked transfer in the PUT
  1791. # request
  1792. def __init__(self, size):
  1793. self.size = size
  1794. def __iter__(self):
  1795. yield b'Z' * self.size
  1796. response = requests.put(path, headers=headers, data=StreamSim(
  1797. self.api_server.image_size_cap + 1))
  1798. self.assertEqual(http.REQUEST_ENTITY_TOO_LARGE, response.status_code)
  1799. # hashlib.md5('Z'*129).hexdigest()
  1800. # == '76522d28cb4418f12704dfa7acd6e7ee'
  1801. # If the image has this checksum, it means that the whole stream was
  1802. # accepted and written to the store, which should not be the case.
  1803. path = self._url('/v2/images/{0}'.format(image_id))
  1804. headers = self._headers({'content-type': 'application/json'})
  1805. response = requests.get(path, headers=headers)
  1806. image_checksum = jsonutils.loads(response.text).get('checksum')
  1807. self.assertNotEqual(image_checksum, '76522d28cb4418f12704dfa7acd6e7ee')
  1808. def test_permissions(self):
  1809. self.start_servers(**self.__dict__.copy())
  1810. # Create an image that belongs to TENANT1
  1811. path = self._url('/v2/images')
  1812. headers = self._headers({'Content-Type': 'application/json'})
  1813. data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'raw',
  1814. 'container_format': 'bare'})
  1815. response = requests.post(path, headers=headers, data=data)
  1816. self.assertEqual(http.CREATED, response.status_code)
  1817. image_id = jsonutils.loads(response.text)['id']
  1818. # Upload some image data
  1819. path = self._url('/v2/images/%s/file' % image_id)
  1820. headers = self._headers({'Content-Type': 'application/octet-stream'})
  1821. response = requests.put(path, headers=headers, data='ZZZZZ')
  1822. self.assertEqual(http.NO_CONTENT, response.status_code)
  1823. # TENANT1 should see the image in their list
  1824. path = self._url('/v2/images')
  1825. response = requests.get(path, headers=self._headers())
  1826. self.assertEqual(http.OK, response.status_code)
  1827. images = jsonutils.loads(response.text)['images']
  1828. self.assertEqual(image_id, images[0]['id'])
  1829. # TENANT1 should be able to access the image directly
  1830. path = self._url('/v2/images/%s' % image_id)
  1831. response = requests.get(path, headers=self._headers())
  1832. self.assertEqual(http.OK, response.status_code)
  1833. # TENANT2 should not see the image in their list
  1834. path = self._url('/v2/images')
  1835. headers = self._headers({'X-Tenant-Id': TENANT2})
  1836. response = requests.get(path, headers=headers)
  1837. self.assertEqual(http.OK, response.status_code)
  1838. images = jsonutils.loads(response.text)['images']
  1839. self.assertEqual(0, len(images))
  1840. # TENANT2 should not be able to access the image directly
  1841. path = self._url('/v2/images/%s' % image_id)
  1842. headers = self._headers({'X-Tenant-Id': TENANT2})
  1843. response = requests.get(path, headers=headers)
  1844. self.assertEqual(http.NOT_FOUND, response.status_code)
  1845. # TENANT2 should not be able to modify the image, either
  1846. path = self._url('/v2/images/%s' % image_id)
  1847. headers = self._headers({
  1848. 'Content-Type': 'application/openstack-images-v2.1-json-patch',
  1849. 'X-Tenant-Id': TENANT2,
  1850. })
  1851. doc = [{'op': 'replace', 'path': '/name', 'value': 'image-2'}]
  1852. data = jsonutils.dumps(doc)
  1853. response = requests.patch(path, headers=headers, data=data)
  1854. self.assertEqual(http.NOT_FOUND, response.status_code)
  1855. # TENANT2 should not be able to delete the image, either
  1856. path = self._url('/v2/images/%s' % image_id)
  1857. headers = self._headers({'X-Tenant-Id': TENANT2})
  1858. response = requests.delete(path, headers=headers)
  1859. self.assertEqual(http.NOT_FOUND, response.status_code)
  1860. # Publicize the image as an admin of TENANT1
  1861. path = self._url('/v2/images/%s' % image_id)
  1862. headers = self._headers({
  1863. 'Content-Type': 'application/openstack-images-v2.1-json-patch',
  1864. 'X-Roles': 'admin',
  1865. })
  1866. doc = [{'op': 'replace', 'path': '/visibility', 'value': 'public'}]
  1867. data = jsonutils.dumps(doc)
  1868. response = requests.patch(path, headers=headers, data=data)
  1869. self.assertEqual(http.OK, response.status_code)
  1870. # TENANT3 should now see the image in their list
  1871. path = self._url('/v2/images')
  1872. headers = self._headers({'X-Tenant-Id': TENANT3})
  1873. response = requests.get(path, headers=headers)
  1874. self.assertEqual(http.OK, response.status_code)
  1875. images = jsonutils.loads(response.text)['images']
  1876. self.assertEqual(image_id, images[0]['id'])
  1877. # TENANT3 should also be able to access the image directly
  1878. path = self._url('/v2/images/%s' % image_id)
  1879. headers = self._headers({'X-Tenant-Id': TENANT3})
  1880. response = requests.get(path, headers=headers)
  1881. self.assertEqual(http.OK, response.status_code)
  1882. # TENANT3 still should not be able to modify the image
  1883. path = self._url('/v2/images/%s' % image_id)
  1884. headers = self._headers({
  1885. 'Content-Type': 'application/openstack-images-v2.1-json-patch',
  1886. 'X-Tenant-Id': TENANT3,
  1887. })
  1888. doc = [{'op': 'replace', 'path': '/name', 'value': 'image-2'}]
  1889. data = jsonutils.dumps(doc)
  1890. response = requests.patch(path, headers=headers, data=data)
  1891. self.assertEqual(http.FORBIDDEN, response.status_code)
  1892. # TENANT3 should not be able to delete the image, either
  1893. path = self._url('/v2/images/%s' % image_id)
  1894. headers = self._headers({'X-Tenant-Id': TENANT3})
  1895. response = requests.delete(path, headers=headers)
  1896. self.assertEqual(http.FORBIDDEN, response.status_code)
  1897. # Image data should still be present after the failed delete
  1898. path = self._url('/v2/images/%s/file' % image_id)
  1899. response = requests.get(path, headers=self._headers())
  1900. self.assertEqual(http.OK, response.status_code)
  1901. self.assertEqual(response.text, 'ZZZZZ')
  1902. self.stop_servers()
  1903. def test_property_protections_with_roles(self):
  1904. # Enable property protection
  1905. self.api_server.property_protection_file = self.property_file_roles
  1906. self.start_servers(**self.__dict__.copy())
  1907. # Image list should be empty
  1908. path = self._url('/v2/images')
  1909. response = requests.get(path, headers=self._headers())
  1910. self.assertEqual(http.OK, response.status_code)
  1911. images = jsonutils.loads(response.text)['images']
  1912. self.assertEqual(0, len(images))
  1913. # Create an image for role member with extra props
  1914. # Raises 403 since user is not allowed to set 'foo'
  1915. path = self._url('/v2/images')
  1916. headers = self._headers({'content-type': 'application/json',
  1917. 'X-Roles': 'member'})
  1918. data = jsonutils.dumps({'name': 'image-1', 'foo': 'bar',
  1919. 'disk_format': 'aki',
  1920. 'container_format': 'aki',
  1921. 'x_owner_foo': 'o_s_bar'})
  1922. response = requests.post(path, headers=headers, data=data)
  1923. self.assertEqual(http.FORBIDDEN, response.status_code)
  1924. # Create an image for role member without 'foo'
  1925. path = self._url('/v2/images')
  1926. headers = self._headers({'content-type': 'application/json',
  1927. 'X-Roles': 'member'})
  1928. data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki',
  1929. 'container_format': 'aki',
  1930. 'x_owner_foo': 'o_s_bar'})
  1931. response = requests.post(path, headers=headers, data=data)
  1932. self.assertEqual(http.CREATED, response.status_code)
  1933. # Returned image entity should have 'x_owner_foo'
  1934. image = jsonutils.loads(response.text)
  1935. image_id = image['id']
  1936. expected_image = {
  1937. 'status': 'queued',
  1938. 'name': 'image-1',
  1939. 'tags': [],
  1940. 'visibility': 'shared',
  1941. 'self': '/v2/images/%s' % image_id,
  1942. 'protected': False,
  1943. 'file': '/v2/images/%s/file' % image_id,
  1944. 'min_disk': 0,
  1945. 'x_owner_foo': 'o_s_bar',
  1946. 'min_ram': 0,
  1947. 'schema': '/v2/schemas/image',
  1948. }
  1949. for key, value in expected_image.items():
  1950. self.assertEqual(value, image[key], key)
  1951. # Create an image for role spl_role with extra props
  1952. path = self._url('/v2/images')
  1953. headers = self._headers({'content-type': 'application/json',
  1954. 'X-Roles': 'spl_role'})
  1955. data = jsonutils.dumps({'name': 'image-1',
  1956. 'disk_format': 'aki',
  1957. 'container_format': 'aki',
  1958. 'spl_create_prop': 'create_bar',
  1959. 'spl_create_prop_policy': 'create_policy_bar',
  1960. 'spl_read_prop': 'read_bar',
  1961. 'spl_update_prop': 'update_bar',
  1962. 'spl_delete_prop': 'delete_bar',
  1963. 'spl_delete_empty_prop': ''})
  1964. response = requests.post(path, headers=headers, data=data)
  1965. self.assertEqual(http.CREATED, response.status_code)
  1966. image = jsonutils.loads(response.text)
  1967. image_id = image['id']
  1968. # Attempt to replace, add and remove properties which are forbidden
  1969. path = self._url('/v2/images/%s' % image_id)
  1970. media_type = 'application/openstack-images-v2.1-json-patch'
  1971. headers = self._headers({'content-type': media_type,
  1972. 'X-Roles': 'spl_role'})
  1973. data = jsonutils.dumps([
  1974. {'op': 'replace', 'path': '/spl_read_prop', 'value': 'r'},
  1975. {'op': 'replace', 'path': '/spl_update_prop', 'value': 'u'},
  1976. ])
  1977. response = requests.patch(path, headers=headers, data=data)
  1978. self.assertEqual(http.FORBIDDEN, response.status_code, response.text)
  1979. # Attempt to replace, add and remove properties which are forbidden
  1980. path = self._url('/v2/images/%s' % image_id)
  1981. media_type = 'application/openstack-images-v2.1-json-patch'
  1982. headers = self._headers({'content-type': media_type,
  1983. 'X-Roles': 'spl_role'})
  1984. data = jsonutils.dumps([
  1985. {'op': 'add', 'path': '/spl_new_prop', 'value': 'new'},
  1986. {'op': 'remove', 'path': '/spl_create_prop'},
  1987. {'op': 'remove', 'path': '/spl_delete_prop'},
  1988. ])
  1989. response = requests.patch(path, headers=headers, data=data)
  1990. self.assertEqual(http.FORBIDDEN, response.status_code, response.text)
  1991. # Attempt to replace properties
  1992. path = self._url('/v2/images/%s' % image_id)
  1993. media_type = 'application/openstack-images-v2.1-json-patch'
  1994. headers = self._headers({'content-type': media_type,
  1995. 'X-Roles': 'spl_role'})
  1996. data = jsonutils.dumps([
  1997. # Updating an empty property to verify bug #1332103.
  1998. {'op': 'replace', 'path': '/spl_update_prop', 'value': ''},
  1999. {'op': 'replace', 'path': '/spl_update_prop', 'value': 'u'},
  2000. ])
  2001. response = requests.patch(path, headers=headers, data=data)
  2002. self.assertEqual(http.OK, response.status_code, response.text)
  2003. # Returned image entity should reflect the changes
  2004. image = jsonutils.loads(response.text)
  2005. # 'spl_update_prop' has update permission for spl_role
  2006. # hence the value has changed
  2007. self.assertEqual('u', image['spl_update_prop'])
  2008. # Attempt to remove properties
  2009. path = self._url('/v2/images/%s' % image_id)
  2010. media_type = 'application/openstack-images-v2.1-json-patch'
  2011. headers = self._headers({'content-type': media_type,
  2012. 'X-Roles': 'spl_role'})
  2013. data = jsonutils.dumps([
  2014. {'op': 'remove', 'path': '/spl_delete_prop'},
  2015. # Deleting an empty property to verify bug #1332103.
  2016. {'op': 'remove', 'path': '/spl_delete_empty_prop'},
  2017. ])
  2018. response = requests.patch(path, headers=headers, data=data)
  2019. self.assertEqual(http.OK, response.status_code, response.text)
  2020. # Returned image entity should reflect the changes
  2021. image = jsonutils.loads(response.text)
  2022. # 'spl_delete_prop' and 'spl_delete_empty_prop' have delete
  2023. # permission for spl_role hence the property has been deleted
  2024. self.assertNotIn('spl_delete_prop', image.keys())
  2025. self.assertNotIn('spl_delete_empty_prop', image.keys())
  2026. # Image Deletion should work
  2027. path = self._url('/v2/images/%s' % image_id)
  2028. response = requests.delete(path, headers=self._headers())
  2029. self.assertEqual(http.NO_CONTENT, response.status_code)
  2030. # This image should be no longer be directly accessible
  2031. path = self._url('/v2/images/%s' % image_id)
  2032. response = requests.get(path, headers=self._headers())
  2033. self.assertEqual(http.NOT_FOUND, response.status_code)
  2034. self.stop_servers()
  2035. def test_property_protections_with_policies(self):
  2036. # Enable property protection
  2037. self.api_server.property_protection_file = self.property_file_policies
  2038. self.api_server.property_protection_rule_format = 'policies'
  2039. self.start_servers(**self.__dict__.copy())
  2040. # Image list should be empty
  2041. path = self._url('/v2/images')
  2042. response = requests.get(path, headers=self._headers())
  2043. self.assertEqual(http.OK, response.status_code)
  2044. images = jsonutils.loads(response.text)['images']
  2045. self.assertEqual(0, len(images))
  2046. # Create an image for role member with extra props
  2047. # Raises 403 since user is not allowed to set 'foo'
  2048. path = self._url('/v2/images')
  2049. headers = self._headers({'content-type': 'application/json',
  2050. 'X-Roles': 'member'})
  2051. data = jsonutils.dumps({'name': 'image-1', 'foo': 'bar',
  2052. 'disk_format': 'aki',
  2053. 'container_format': 'aki',
  2054. 'x_owner_foo': 'o_s_bar'})
  2055. response = requests.post(path, headers=headers, data=data)
  2056. self.assertEqual(http.FORBIDDEN, response.status_code)
  2057. # Create an image for role member without 'foo'
  2058. path = self._url('/v2/images')
  2059. headers = self._headers({'content-type': 'application/json',
  2060. 'X-Roles': 'member'})
  2061. data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki',
  2062. 'container_format': 'aki'})
  2063. response = requests.post(path, headers=headers, data=data)
  2064. self.assertEqual(http.CREATED, response.status_code)
  2065. # Returned image entity
  2066. image = jsonutils.loads(response.text)
  2067. image_id = image['id']
  2068. expected_image = {
  2069. 'status': 'queued',
  2070. 'name': 'image-1',
  2071. 'tags': [],
  2072. 'visibility': 'shared',
  2073. 'self': '/v2/images/%s' % image_id,
  2074. 'protected': False,
  2075. 'file': '/v2/images/%s/file' % image_id,
  2076. 'min_disk': 0,
  2077. 'min_ram': 0,
  2078. 'schema': '/v2/schemas/image',
  2079. }
  2080. for key, value in expected_image.items():
  2081. self.assertEqual(value, image[key], key)
  2082. # Create an image for role spl_role with extra props
  2083. path = self._url('/v2/images')
  2084. headers = self._headers({'content-type': 'application/json',
  2085. 'X-Roles': 'spl_role, admin'})
  2086. data = jsonutils.dumps({'name': 'image-1',
  2087. 'disk_format': 'aki',
  2088. 'container_format': 'aki',
  2089. 'spl_creator_policy': 'creator_bar',
  2090. 'spl_default_policy': 'default_bar'})
  2091. response = requests.post(path, headers=headers, data=data)
  2092. self.assertEqual(http.CREATED, response.status_code)
  2093. image = jsonutils.loads(response.text)
  2094. image_id = image['id']
  2095. self.assertEqual('creator_bar', image['spl_creator_policy'])
  2096. self.assertEqual('default_bar', image['spl_default_policy'])
  2097. # Attempt to replace a property which is permitted
  2098. path = self._url('/v2/images/%s' % image_id)
  2099. media_type = 'application/openstack-images-v2.1-json-patch'
  2100. headers = self._headers({'content-type': media_type,
  2101. 'X-Roles': 'admin'})
  2102. data = jsonutils.dumps([
  2103. # Updating an empty property to verify bug #1332103.
  2104. {'op': 'replace', 'path': '/spl_creator_policy', 'value': ''},
  2105. {'op': 'replace', 'path': '/spl_creator_policy', 'value': 'r'},
  2106. ])
  2107. response = requests.patch(path, headers=headers, data=data)
  2108. self.assertEqual(http.OK, response.status_code, response.text)
  2109. # Returned image entity should reflect the changes
  2110. image = jsonutils.loads(response.text)
  2111. # 'spl_creator_policy' has update permission for admin
  2112. # hence the value has changed
  2113. self.assertEqual('r', image['spl_creator_policy'])
  2114. # Attempt to replace a property which is forbidden
  2115. path = self._url('/v2/images/%s' % image_id)
  2116. media_type = 'application/openstack-images-v2.1-json-patch'
  2117. headers = self._headers({'content-type': media_type,
  2118. 'X-Roles': 'spl_role'})
  2119. data = jsonutils.dumps([
  2120. {'op': 'replace', 'path': '/spl_creator_policy', 'value': 'z'},
  2121. ])
  2122. response = requests.patch(path, headers=headers, data=data)
  2123. self.assertEqual(http.FORBIDDEN, response.status_code, response.text)
  2124. # Attempt to read properties
  2125. path = self._url('/v2/images/%s' % image_id)
  2126. headers = self._headers({'content-type': media_type,
  2127. 'X-Roles': 'random_role'})
  2128. response = requests.get(path, headers=headers)
  2129. self.assertEqual(http.OK, response.status_code)
  2130. image = jsonutils.loads(response.text)
  2131. # 'random_role' is allowed read 'spl_default_policy'.
  2132. self.assertEqual(image['spl_default_policy'], 'default_bar')
  2133. # 'random_role' is forbidden to read 'spl_creator_policy'.
  2134. self.assertNotIn('spl_creator_policy', image)
  2135. # Attempt to replace and remove properties which are permitted
  2136. path = self._url('/v2/images/%s' % image_id)
  2137. media_type = 'application/openstack-images-v2.1-json-patch'
  2138. headers = self._headers({'content-type': media_type,
  2139. 'X-Roles': 'admin'})
  2140. data = jsonutils.dumps([
  2141. # Deleting an empty property to verify bug #1332103.
  2142. {'op': 'replace', 'path': '/spl_creator_policy', 'value': ''},
  2143. {'op': 'remove', 'path': '/spl_creator_policy'},
  2144. ])
  2145. response = requests.patch(path, headers=headers, data=data)
  2146. self.assertEqual(http.OK, response.status_code, response.text)
  2147. # Returned image entity should reflect the changes
  2148. image = jsonutils.loads(response.text)
  2149. # 'spl_creator_policy' has delete permission for admin
  2150. # hence the value has been deleted
  2151. self.assertNotIn('spl_creator_policy', image)
  2152. # Attempt to read a property that is permitted
  2153. path = self._url('/v2/images/%s' % image_id)
  2154. headers = self._headers({'content-type': media_type,
  2155. 'X-Roles': 'random_role'})
  2156. response = requests.get(path, headers=headers)
  2157. self.assertEqual(http.OK, response.status_code)
  2158. # Returned image entity should reflect the changes
  2159. image = jsonutils.loads(response.text)
  2160. self.assertEqual(image['spl_default_policy'], 'default_bar')
  2161. # Image Deletion should work
  2162. path = self._url('/v2/images/%s' % image_id)
  2163. response = requests.delete(path, headers=self._headers())
  2164. self.assertEqual(http.NO_CONTENT, response.status_code)
  2165. # This image should be no longer be directly accessible
  2166. path = self._url('/v2/images/%s' % image_id)
  2167. response = requests.get(path, headers=self._headers())
  2168. self.assertEqual(http.NOT_FOUND, response.status_code)
  2169. self.stop_servers()
  2170. def test_property_protections_special_chars_roles(self):
  2171. # Enable property protection
  2172. self.api_server.property_protection_file = self.property_file_roles
  2173. self.start_servers(**self.__dict__.copy())
  2174. # Verify both admin and unknown role can create properties marked with
  2175. # '@'
  2176. path = self._url('/v2/images')
  2177. headers = self._headers({'content-type': 'application/json',
  2178. 'X-Roles': 'admin'})
  2179. data = jsonutils.dumps({
  2180. 'name': 'image-1',
  2181. 'disk_format': 'aki',
  2182. 'container_format': 'aki',
  2183. 'x_all_permitted_admin': '1'
  2184. })
  2185. response = requests.post(path, headers=headers, data=data)
  2186. self.assertEqual(http.CREATED, response.status_code)
  2187. image = jsonutils.loads(response.text)
  2188. image_id = image['id']
  2189. expected_image = {
  2190. 'status': 'queued',
  2191. 'name': 'image-1',
  2192. 'tags': [],
  2193. 'visibility': 'shared',
  2194. 'self': '/v2/images/%s' % image_id,
  2195. 'protected': False,
  2196. 'file': '/v2/images/%s/file' % image_id,
  2197. 'min_disk': 0,
  2198. 'x_all_permitted_admin': '1',
  2199. 'min_ram': 0,
  2200. 'schema': '/v2/schemas/image',
  2201. }
  2202. for key, value in expected_image.items():
  2203. self.assertEqual(value, image[key], key)
  2204. path = self._url('/v2/images')
  2205. headers = self._headers({'content-type': 'application/json',
  2206. 'X-Roles': 'joe_soap'})
  2207. data = jsonutils.dumps({
  2208. 'name': 'image-1',
  2209. 'disk_format': 'aki',
  2210. 'container_format': 'aki',
  2211. 'x_all_permitted_joe_soap': '1'
  2212. })
  2213. response = requests.post(path, headers=headers, data=data)
  2214. self.assertEqual(http.CREATED, response.status_code)
  2215. image = jsonutils.loads(response.text)
  2216. image_id = image['id']
  2217. expected_image = {
  2218. 'status': 'queued',
  2219. 'name': 'image-1',
  2220. 'tags': [],
  2221. 'visibility': 'shared',
  2222. 'self': '/v2/images/%s' % image_id,
  2223. 'protected': False,
  2224. 'file': '/v2/images/%s/file' % image_id,
  2225. 'min_disk': 0,
  2226. 'x_all_permitted_joe_soap': '1',
  2227. 'min_ram': 0,
  2228. 'schema': '/v2/schemas/image',
  2229. }
  2230. for key, value in expected_image.items():
  2231. self.assertEqual(value, image[key], key)
  2232. # Verify both admin and unknown role can read properties marked with
  2233. # '@'
  2234. headers = self._headers({'content-type': 'application/json',
  2235. 'X-Roles': 'admin'})
  2236. path = self._url('/v2/images/%s' % image_id)
  2237. response = requests.get(path, headers=self._headers())
  2238. self.assertEqual(http.OK, response.status_code)
  2239. image = jsonutils.loads(response.text)
  2240. self.assertEqual('1', image['x_all_permitted_joe_soap'])
  2241. headers = self._headers({'content-type': 'application/json',
  2242. 'X-Roles': 'joe_soap'})
  2243. path = self._url('/v2/images/%s' % image_id)
  2244. response = requests.get(path, headers=self._headers())
  2245. self.assertEqual(http.OK, response.status_code)
  2246. image = jsonutils.loads(response.text)
  2247. self.assertEqual('1', image['x_all_permitted_joe_soap'])
  2248. # Verify both admin and unknown role can update properties marked with
  2249. # '@'
  2250. path = self._url('/v2/images/%s' % image_id)
  2251. media_type = 'application/openstack-images-v2.1-json-patch'
  2252. headers = self._headers({'content-type': media_type,
  2253. 'X-Roles': 'admin'})
  2254. data = jsonutils.dumps([
  2255. {'op': 'replace',
  2256. 'path': '/x_all_permitted_joe_soap', 'value': '2'}
  2257. ])
  2258. response = requests.patch(path, headers=headers, data=data)
  2259. self.assertEqual(http.OK, response.status_code, response.text)
  2260. image = jsonutils.loads(response.text)
  2261. self.assertEqual('2', image['x_all_permitted_joe_soap'])
  2262. path = self._url('/v2/images/%s' % image_id)
  2263. media_type = 'application/openstack-images-v2.1-json-patch'
  2264. headers = self._headers({'content-type': media_type,
  2265. 'X-Roles': 'joe_soap'})
  2266. data = jsonutils.dumps([
  2267. {'op': 'replace',
  2268. 'path': '/x_all_permitted_joe_soap', 'value': '3'}
  2269. ])
  2270. response = requests.patch(path, headers=headers, data=data)
  2271. self.assertEqual(http.OK, response.status_code, response.text)
  2272. image = jsonutils.loads(response.text)
  2273. self.assertEqual('3', image['x_all_permitted_joe_soap'])
  2274. # Verify both admin and unknown role can delete properties marked with
  2275. # '@'
  2276. path = self._url('/v2/images')
  2277. headers = self._headers({'content-type': 'application/json',
  2278. 'X-Roles': 'admin'})
  2279. data = jsonutils.dumps({
  2280. 'name': 'image-1',
  2281. 'disk_format': 'aki',
  2282. 'container_format': 'aki',
  2283. 'x_all_permitted_a': '1',
  2284. 'x_all_permitted_b': '2'
  2285. })
  2286. response = requests.post(path, headers=headers, data=data)
  2287. self.assertEqual(http.CREATED, response.status_code)
  2288. image = jsonutils.loads(response.text)
  2289. image_id = image['id']
  2290. path = self._url('/v2/images/%s' % image_id)
  2291. media_type = 'application/openstack-images-v2.1-json-patch'
  2292. headers = self._headers({'content-type': media_type,
  2293. 'X-Roles': 'admin'})
  2294. data = jsonutils.dumps([
  2295. {'op': 'remove', 'path': '/x_all_permitted_a'}
  2296. ])
  2297. response = requests.patch(path, headers=headers, data=data)
  2298. self.assertEqual(http.OK, response.status_code, response.text)
  2299. image = jsonutils.loads(response.text)
  2300. self.assertNotIn('x_all_permitted_a', image.keys())
  2301. path = self._url('/v2/images/%s' % image_id)
  2302. media_type = 'application/openstack-images-v2.1-json-patch'
  2303. headers = self._headers({'content-type': media_type,
  2304. 'X-Roles': 'joe_soap'})
  2305. data = jsonutils.dumps([
  2306. {'op': 'remove', 'path': '/x_all_permitted_b'}
  2307. ])
  2308. response = requests.patch(path, headers=headers, data=data)
  2309. self.assertEqual(http.OK, response.status_code, response.text)
  2310. image = jsonutils.loads(response.text)
  2311. self.assertNotIn('x_all_permitted_b', image.keys())
  2312. # Verify neither admin nor unknown role can create a property protected
  2313. # with '!'
  2314. path = self._url('/v2/images')
  2315. headers = self._headers({'content-type': 'application/json',
  2316. 'X-Roles': 'admin'})
  2317. data = jsonutils.dumps({
  2318. 'name': 'image-1',
  2319. 'disk_format': 'aki',
  2320. 'container_format': 'aki',
  2321. 'x_none_permitted_admin': '1'
  2322. })
  2323. response = requests.post(path, headers=headers, data=data)
  2324. self.assertEqual(http.FORBIDDEN, response.status_code)
  2325. path = self._url('/v2/images')
  2326. headers = self._headers({'content-type': 'application/json',
  2327. 'X-Roles': 'joe_soap'})
  2328. data = jsonutils.dumps({
  2329. 'name': 'image-1',
  2330. 'disk_format': 'aki',
  2331. 'container_format': 'aki',
  2332. 'x_none_permitted_joe_soap': '1'
  2333. })
  2334. response = requests.post(path, headers=headers, data=data)
  2335. self.assertEqual(http.FORBIDDEN, response.status_code)
  2336. # Verify neither admin nor unknown role can read properties marked with
  2337. # '!'
  2338. path = self._url('/v2/images')
  2339. headers = self._headers({'content-type': 'application/json',
  2340. 'X-Roles': 'admin'})
  2341. data = jsonutils.dumps({
  2342. 'name': 'image-1',
  2343. 'disk_format': 'aki',
  2344. 'container_format': 'aki',
  2345. 'x_none_read': '1'
  2346. })
  2347. response = requests.post(path, headers=headers, data=data)
  2348. self.assertEqual(http.CREATED, response.status_code)
  2349. image = jsonutils.loads(response.text)
  2350. image_id = image['id']
  2351. self.assertNotIn('x_none_read', image.keys())
  2352. headers = self._headers({'content-type': 'application/json',
  2353. 'X-Roles': 'admin'})
  2354. path = self._url('/v2/images/%s' % image_id)
  2355. response = requests.get(path, headers=self._headers())
  2356. self.assertEqual(http.OK, response.status_code)
  2357. image = jsonutils.loads(response.text)
  2358. self.assertNotIn('x_none_read', image.keys())
  2359. headers = self._headers({'content-type': 'application/json',
  2360. 'X-Roles': 'joe_soap'})
  2361. path = self._url('/v2/images/%s' % image_id)
  2362. response = requests.get(path, headers=self._headers())
  2363. self.assertEqual(http.OK, response.status_code)
  2364. image = jsonutils.loads(response.text)
  2365. self.assertNotIn('x_none_read', image.keys())
  2366. # Verify neither admin nor unknown role can update properties marked
  2367. # with '!'
  2368. path = self._url('/v2/images')
  2369. headers = self._headers({'content-type': 'application/json',
  2370. 'X-Roles': 'admin'})
  2371. data = jsonutils.dumps({
  2372. 'name': 'image-1',
  2373. 'disk_format': 'aki',
  2374. 'container_format': 'aki',
  2375. 'x_none_update': '1'
  2376. })
  2377. response = requests.post(path, headers=headers, data=data)
  2378. self.assertEqual(http.CREATED, response.status_code)
  2379. image = jsonutils.loads(response.text)
  2380. image_id = image['id']
  2381. self.assertEqual('1', image['x_none_update'])
  2382. path = self._url('/v2/images/%s' % image_id)
  2383. media_type = 'application/openstack-images-v2.1-json-patch'
  2384. headers = self._headers({'content-type': media_type,
  2385. 'X-Roles': 'admin'})
  2386. data = jsonutils.dumps([
  2387. {'op': 'replace',
  2388. 'path': '/x_none_update', 'value': '2'}
  2389. ])
  2390. response = requests.patch(path, headers=headers, data=data)
  2391. self.assertEqual(http.FORBIDDEN, response.status_code, response.text)
  2392. path = self._url('/v2/images/%s' % image_id)
  2393. media_type = 'application/openstack-images-v2.1-json-patch'
  2394. headers = self._headers({'content-type': media_type,
  2395. 'X-Roles': 'joe_soap'})
  2396. data = jsonutils.dumps([
  2397. {'op': 'replace',
  2398. 'path': '/x_none_update', 'value': '3'}
  2399. ])
  2400. response = requests.patch(path, headers=headers, data=data)
  2401. self.assertEqual(http.CONFLICT, response.status_code, response.text)
  2402. # Verify neither admin nor unknown role can delete properties marked
  2403. # with '!'
  2404. path = self._url('/v2/images')
  2405. headers = self._headers({'content-type': 'application/json',
  2406. 'X-Roles': 'admin'})
  2407. data = jsonutils.dumps({
  2408. 'name': 'image-1',
  2409. 'disk_format': 'aki',
  2410. 'container_format': 'aki',
  2411. 'x_none_delete': '1',
  2412. })
  2413. response = requests.post(path, headers=headers, data=data)
  2414. self.assertEqual(http.CREATED, response.status_code)
  2415. image = jsonutils.loads(response.text)
  2416. image_id = image['id']
  2417. path = self._url('/v2/images/%s' % image_id)
  2418. media_type = 'application/openstack-images-v2.1-json-patch'
  2419. headers = self._headers({'content-type': media_type,
  2420. 'X-Roles': 'admin'})
  2421. data = jsonutils.dumps([
  2422. {'op': 'remove', 'path': '/x_none_delete'}
  2423. ])
  2424. response = requests.patch(path, headers=headers, data=data)
  2425. self.assertEqual(http.FORBIDDEN, response.status_code, response.text)
  2426. path = self._url('/v2/images/%s' % image_id)
  2427. media_type = 'application/openstack-images-v2.1-json-patch'
  2428. headers = self._headers({'content-type': media_type,
  2429. 'X-Roles': 'joe_soap'})
  2430. data = jsonutils.dumps([
  2431. {'op': 'remove', 'path': '/x_none_delete'}
  2432. ])
  2433. response = requests.patch(path, headers=headers, data=data)
  2434. self.assertEqual(http.CONFLICT, response.status_code, response.text)
  2435. self.stop_servers()
  2436. def test_property_protections_special_chars_policies(self):
  2437. # Enable property protection
  2438. self.api_server.property_protection_file = self.property_file_policies
  2439. self.api_server.property_protection_rule_format = 'policies'
  2440. self.start_servers(**self.__dict__.copy())
  2441. # Verify both admin and unknown role can create properties marked with
  2442. # '@'
  2443. path = self._url('/v2/images')
  2444. headers = self._headers({'content-type': 'application/json',
  2445. 'X-Roles': 'admin'})
  2446. data = jsonutils.dumps({
  2447. 'name': 'image-1',
  2448. 'disk_format': 'aki',
  2449. 'container_format': 'aki',
  2450. 'x_all_permitted_admin': '1'
  2451. })
  2452. response = requests.post(path, headers=headers, data=data)
  2453. self.assertEqual(http.CREATED, response.status_code)
  2454. image = jsonutils.loads(response.text)
  2455. image_id = image['id']
  2456. expected_image = {
  2457. 'status': 'queued',
  2458. 'name': 'image-1',
  2459. 'tags': [],
  2460. 'visibility': 'shared',
  2461. 'self': '/v2/images/%s' % image_id,
  2462. 'protected': False,
  2463. 'file': '/v2/images/%s/file' % image_id,
  2464. 'min_disk': 0,
  2465. 'x_all_permitted_admin': '1',
  2466. 'min_ram': 0,
  2467. 'schema': '/v2/schemas/image',
  2468. }
  2469. for key, value in expected_image.items():
  2470. self.assertEqual(value, image[key], key)
  2471. path = self._url('/v2/images')
  2472. headers = self._headers({'content-type': 'application/json',
  2473. 'X-Roles': 'joe_soap'})
  2474. data = jsonutils.dumps({
  2475. 'name': 'image-1',
  2476. 'disk_format': 'aki',
  2477. 'container_format': 'aki',
  2478. 'x_all_permitted_joe_soap': '1'
  2479. })
  2480. response = requests.post(path, headers=headers, data=data)
  2481. self.assertEqual(http.CREATED, response.status_code)
  2482. image = jsonutils.loads(response.text)
  2483. image_id = image['id']
  2484. expected_image = {
  2485. 'status': 'queued',
  2486. 'name': 'image-1',
  2487. 'tags': [],
  2488. 'visibility': 'shared',
  2489. 'self': '/v2/images/%s' % image_id,
  2490. 'protected': False,
  2491. 'file': '/v2/images/%s/file' % image_id,
  2492. 'min_disk': 0,
  2493. 'x_all_permitted_joe_soap': '1',
  2494. 'min_ram': 0,
  2495. 'schema': '/v2/schemas/image',
  2496. }
  2497. for key, value in expected_image.items():
  2498. self.assertEqual(value, image[key], key)
  2499. # Verify both admin and unknown role can read properties marked with
  2500. # '@'
  2501. headers = self._headers({'content-type': 'application/json',
  2502. 'X-Roles': 'admin'})
  2503. path = self._url('/v2/images/%s' % image_id)
  2504. response = requests.get(path, headers=self._headers())
  2505. self.assertEqual(http.OK, response.status_code)
  2506. image = jsonutils.loads(response.text)
  2507. self.assertEqual('1', image['x_all_permitted_joe_soap'])
  2508. headers = self._headers({'content-type': 'application/json',
  2509. 'X-Roles': 'joe_soap'})
  2510. path = self._url('/v2/images/%s' % image_id)
  2511. response = requests.get(path, headers=self._headers())
  2512. self.assertEqual(http.OK, response.status_code)
  2513. image = jsonutils.loads(response.text)
  2514. self.assertEqual('1', image['x_all_permitted_joe_soap'])
  2515. # Verify both admin and unknown role can update properties marked with
  2516. # '@'
  2517. path = self._url('/v2/images/%s' % image_id)
  2518. media_type = 'application/openstack-images-v2.1-json-patch'
  2519. headers = self._headers({'content-type': media_type,
  2520. 'X-Roles': 'admin'})
  2521. data = jsonutils.dumps([
  2522. {'op': 'replace',
  2523. 'path': '/x_all_permitted_joe_soap', 'value': '2'}
  2524. ])
  2525. response = requests.patch(path, headers=headers, data=data)
  2526. self.assertEqual(http.OK, response.status_code, response.text)
  2527. image = jsonutils.loads(response.text)
  2528. self.assertEqual('2', image['x_all_permitted_joe_soap'])
  2529. path = self._url('/v2/images/%s' % image_id)
  2530. media_type = 'application/openstack-images-v2.1-json-patch'
  2531. headers = self._headers({'content-type': media_type,
  2532. 'X-Roles': 'joe_soap'})
  2533. data = jsonutils.dumps([
  2534. {'op': 'replace',
  2535. 'path': '/x_all_permitted_joe_soap', 'value': '3'}
  2536. ])
  2537. response = requests.patch(path, headers=headers, data=data)
  2538. self.assertEqual(http.OK, response.status_code, response.text)
  2539. image = jsonutils.loads(response.text)
  2540. self.assertEqual('3', image['x_all_permitted_joe_soap'])
  2541. # Verify both admin and unknown role can delete properties marked with
  2542. # '@'
  2543. path = self._url('/v2/images')
  2544. headers = self._headers({'content-type': 'application/json',
  2545. 'X-Roles': 'admin'})
  2546. data = jsonutils.dumps({
  2547. 'name': 'image-1',
  2548. 'disk_format': 'aki',
  2549. 'container_format': 'aki',
  2550. 'x_all_permitted_a': '1',
  2551. 'x_all_permitted_b': '2'
  2552. })
  2553. response = requests.post(path, headers=headers, data=data)
  2554. self.assertEqual(http.CREATED, response.status_code)
  2555. image = jsonutils.loads(response.text)
  2556. image_id = image['id']
  2557. path = self._url('/v2/images/%s' % image_id)
  2558. media_type = 'application/openstack-images-v2.1-json-patch'
  2559. headers = self._headers({'content-type': media_type,
  2560. 'X-Roles': 'admin'})
  2561. data = jsonutils.dumps([
  2562. {'op': 'remove', 'path': '/x_all_permitted_a'}
  2563. ])
  2564. response = requests.patch(path, headers=headers, data=data)
  2565. self.assertEqual(http.OK, response.status_code, response.text)
  2566. image = jsonutils.loads(response.text)
  2567. self.assertNotIn('x_all_permitted_a', image.keys())
  2568. path = self._url('/v2/images/%s' % image_id)
  2569. media_type = 'application/openstack-images-v2.1-json-patch'
  2570. headers = self._headers({'content-type': media_type,
  2571. 'X-Roles': 'joe_soap'})
  2572. data = jsonutils.dumps([
  2573. {'op': 'remove', 'path': '/x_all_permitted_b'}
  2574. ])
  2575. response = requests.patch(path, headers=headers, data=data)
  2576. self.assertEqual(http.OK, response.status_code, response.text)
  2577. image = jsonutils.loads(response.text)
  2578. self.assertNotIn('x_all_permitted_b', image.keys())
  2579. # Verify neither admin nor unknown role can create a property protected
  2580. # with '!'
  2581. path = self._url('/v2/images')
  2582. headers = self._headers({'content-type': 'application/json',
  2583. 'X-Roles': 'admin'})
  2584. data = jsonutils.dumps({
  2585. 'name': 'image-1',
  2586. 'disk_format': 'aki',
  2587. 'container_format': 'aki',
  2588. 'x_none_permitted_admin': '1'
  2589. })
  2590. response = requests.post(path, headers=headers, data=data)
  2591. self.assertEqual(http.FORBIDDEN, response.status_code)
  2592. path = self._url('/v2/images')
  2593. headers = self._headers({'content-type': 'application/json',
  2594. 'X-Roles': 'joe_soap'})
  2595. data = jsonutils.dumps({
  2596. 'name': 'image-1',
  2597. 'disk_format': 'aki',
  2598. 'container_format': 'aki',
  2599. 'x_none_permitted_joe_soap': '1'
  2600. })
  2601. response = requests.post(path, headers=headers, data=data)
  2602. self.assertEqual(http.FORBIDDEN, response.status_code)
  2603. # Verify neither admin nor unknown role can read properties marked with
  2604. # '!'
  2605. path = self._url('/v2/images')
  2606. headers = self._headers({'content-type': 'application/json',
  2607. 'X-Roles': 'admin'})
  2608. data = jsonutils.dumps({
  2609. 'name': 'image-1',
  2610. 'disk_format': 'aki',
  2611. 'container_format': 'aki',
  2612. 'x_none_read': '1'
  2613. })
  2614. response = requests.post(path, headers=headers, data=data)
  2615. self.assertEqual(http.CREATED, response.status_code)
  2616. image = jsonutils.loads(response.text)
  2617. image_id = image['id']
  2618. self.assertNotIn('x_none_read', image.keys())
  2619. headers = self._headers({'content-type': 'application/json',
  2620. 'X-Roles': 'admin'})
  2621. path = self._url('/v2/images/%s' % image_id)
  2622. response = requests.get(path, headers=self._headers())
  2623. self.assertEqual(http.OK, response.status_code)
  2624. image = jsonutils.loads(response.text)
  2625. self.assertNotIn('x_none_read', image.keys())
  2626. headers = self._headers({'content-type': 'application/json',
  2627. 'X-Roles': 'joe_soap'})
  2628. path = self._url('/v2/images/%s' % image_id)
  2629. response = requests.get(path, headers=self._headers())
  2630. self.assertEqual(http.OK, response.status_code)
  2631. image = jsonutils.loads(response.text)
  2632. self.assertNotIn('x_none_read', image.keys())
  2633. # Verify neither admin nor unknown role can update properties marked
  2634. # with '!'
  2635. path = self._url('/v2/images')
  2636. headers = self._headers({'content-type': 'application/json',
  2637. 'X-Roles': 'admin'})
  2638. data = jsonutils.dumps({
  2639. 'name': 'image-1',
  2640. 'disk_format': 'aki',
  2641. 'container_format': 'aki',
  2642. 'x_none_update': '1'
  2643. })
  2644. response = requests.post(path, headers=headers, data=data)
  2645. self.assertEqual(http.CREATED, response.status_code)
  2646. image = jsonutils.loads(response.text)
  2647. image_id = image['id']
  2648. self.assertEqual('1', image['x_none_update'])
  2649. path = self._url('/v2/images/%s' % image_id)
  2650. media_type = 'application/openstack-images-v2.1-json-patch'
  2651. headers = self._headers({'content-type': media_type,
  2652. 'X-Roles': 'admin'})
  2653. data = jsonutils.dumps([
  2654. {'op': 'replace',
  2655. 'path': '/x_none_update', 'value': '2'}
  2656. ])
  2657. response = requests.patch(path, headers=headers, data=data)
  2658. self.assertEqual(http.FORBIDDEN, response.status_code, response.text)
  2659. path = self._url('/v2/images/%s' % image_id)
  2660. media_type = 'application/openstack-images-v2.1-json-patch'
  2661. headers = self._headers({'content-type': media_type,
  2662. 'X-Roles': 'joe_soap'})
  2663. data = jsonutils.dumps([
  2664. {'op': 'replace',
  2665. 'path': '/x_none_update', 'value': '3'}
  2666. ])
  2667. response = requests.patch(path, headers=headers, data=data)
  2668. self.assertEqual(http.CONFLICT, response.status_code, response.text)
  2669. # Verify neither admin nor unknown role can delete properties marked
  2670. # with '!'
  2671. path = self._url('/v2/images')
  2672. headers = self._headers({'content-type': 'application/json',
  2673. 'X-Roles': 'admin'})
  2674. data = jsonutils.dumps({
  2675. 'name': 'image-1',
  2676. 'disk_format': 'aki',
  2677. 'container_format': 'aki',
  2678. 'x_none_delete': '1',
  2679. })
  2680. response = requests.post(path, headers=headers, data=data)
  2681. self.assertEqual(http.CREATED, response.status_code)
  2682. image = jsonutils.loads(response.text)
  2683. image_id = image['id']
  2684. path = self._url('/v2/images/%s' % image_id)
  2685. media_type = 'application/openstack-images-v2.1-json-patch'
  2686. headers = self._headers({'content-type': media_type,
  2687. 'X-Roles': 'admin'})
  2688. data = jsonutils.dumps([
  2689. {'op': 'remove', 'path': '/x_none_delete'}
  2690. ])
  2691. response = requests.patch(path, headers=headers, data=data)
  2692. self.assertEqual(http.FORBIDDEN, response.status_code, response.text)
  2693. path = self._url('/v2/images/%s' % image_id)
  2694. media_type = 'application/openstack-images-v2.1-json-patch'
  2695. headers = self._headers({'content-type': media_type,
  2696. 'X-Roles': 'joe_soap'})
  2697. data = jsonutils.dumps([
  2698. {'op': 'remove', 'path': '/x_none_delete'}
  2699. ])
  2700. response = requests.patch(path, headers=headers, data=data)
  2701. self.assertEqual(http.CONFLICT, response.status_code, response.text)
  2702. self.stop_servers()
  2703. def test_tag_lifecycle(self):
  2704. self.start_servers(**self.__dict__.copy())
  2705. # Create an image with a tag - duplicate should be ignored
  2706. path = self._url('/v2/images')
  2707. headers = self._headers({'Content-Type': 'application/json'})
  2708. data = jsonutils.dumps({'name': 'image-1', 'tags': ['sniff', 'sniff']})
  2709. response = requests.post(path, headers=headers, data=data)
  2710. self.assertEqual(http.CREATED, response.status_code)
  2711. image_id = jsonutils.loads(response.text)['id']
  2712. # Image should show a list with a single tag
  2713. path = self._url('/v2/images/%s' % image_id)
  2714. response = requests.get(path, headers=self._headers())
  2715. self.assertEqual(http.OK, response.status_code)
  2716. tags = jsonutils.loads(response.text)['tags']
  2717. self.assertEqual(['sniff'], tags)
  2718. # Delete all tags
  2719. for tag in tags:
  2720. path = self._url('/v2/images/%s/tags/%s' % (image_id, tag))
  2721. response = requests.delete(path, headers=self._headers())
  2722. self.assertEqual(http.NO_CONTENT, response.status_code)
  2723. # Update image with too many tags via PUT
  2724. # Configured limit is 10 tags
  2725. for i in range(10):
  2726. path = self._url('/v2/images/%s/tags/foo%i' % (image_id, i))
  2727. response = requests.put(path, headers=self._headers())
  2728. self.assertEqual(http.NO_CONTENT, response.status_code)
  2729. # 11th tag should fail
  2730. path = self._url('/v2/images/%s/tags/fail_me' % image_id)
  2731. response = requests.put(path, headers=self._headers())
  2732. self.assertEqual(http.REQUEST_ENTITY_TOO_LARGE, response.status_code)
  2733. # Make sure the 11th tag was not added
  2734. path = self._url('/v2/images/%s' % image_id)
  2735. response = requests.get(path, headers=self._headers())
  2736. self.assertEqual(http.OK, response.status_code)
  2737. tags = jsonutils.loads(response.text)['tags']
  2738. self.assertEqual(10, len(tags))
  2739. # Update image tags via PATCH
  2740. path = self._url('/v2/images/%s' % image_id)
  2741. media_type = 'application/openstack-images-v2.1-json-patch'
  2742. headers = self._headers({'content-type': media_type})
  2743. doc = [
  2744. {
  2745. 'op': 'replace',
  2746. 'path': '/tags',
  2747. 'value': ['foo'],
  2748. },
  2749. ]
  2750. data = jsonutils.dumps(doc)
  2751. response = requests.patch(path, headers=headers, data=data)
  2752. self.assertEqual(http.OK, response.status_code)
  2753. # Update image with too many tags via PATCH
  2754. # Configured limit is 10 tags
  2755. path = self._url('/v2/images/%s' % image_id)
  2756. media_type = 'application/openstack-images-v2.1-json-patch'
  2757. headers = self._headers({'content-type': media_type})
  2758. tags = ['foo%d' % i for i in range(11)]
  2759. doc = [
  2760. {
  2761. 'op': 'replace',
  2762. 'path': '/tags',
  2763. 'value': tags,
  2764. },
  2765. ]
  2766. data = jsonutils.dumps(doc)
  2767. response = requests.patch(path, headers=headers, data=data)
  2768. self.assertEqual(http.REQUEST_ENTITY_TOO_LARGE, response.status_code)
  2769. # Tags should not have changed since request was over limit
  2770. path = self._url('/v2/images/%s' % image_id)
  2771. response = requests.get(path, headers=self._headers())
  2772. self.assertEqual(http.OK, response.status_code)
  2773. tags = jsonutils.loads(response.text)['tags']
  2774. self.assertEqual(['foo'], tags)
  2775. # Update image with duplicate tag - it should be ignored
  2776. path = self._url('/v2/images/%s' % image_id)
  2777. media_type = 'application/openstack-images-v2.1-json-patch'
  2778. headers = self._headers({'content-type': media_type})
  2779. doc = [
  2780. {
  2781. 'op': 'replace',
  2782. 'path': '/tags',
  2783. 'value': ['sniff', 'snozz', 'snozz'],
  2784. },
  2785. ]
  2786. data = jsonutils.dumps(doc)
  2787. response = requests.patch(path, headers=headers, data=data)
  2788. self.assertEqual(http.OK, response.status_code)
  2789. tags = jsonutils.loads(response.text)['tags']
  2790. self.assertEqual(['sniff', 'snozz'], sorted(tags))
  2791. # Image should show the appropriate tags
  2792. path = self._url('/v2/images/%s' % image_id)
  2793. response = requests.get(path, headers=self._headers())
  2794. self.assertEqual(http.OK, response.status_code)
  2795. tags = jsonutils.loads(response.text)['tags']
  2796. self.assertEqual(['sniff', 'snozz'], sorted(tags))
  2797. # Attempt to tag the image with a duplicate should be ignored
  2798. path = self._url('/v2/images/%s/tags/snozz' % image_id)
  2799. response = requests.put(path, headers=self._headers())
  2800. self.assertEqual(http.NO_CONTENT, response.status_code)
  2801. # Create another more complex tag
  2802. path = self._url('/v2/images/%s/tags/gabe%%40example.com' % image_id)
  2803. response = requests.put(path, headers=self._headers())
  2804. self.assertEqual(http.NO_CONTENT, response.status_code)
  2805. # Double-check that the tags container on the image is populated
  2806. path = self._url('/v2/images/%s' % image_id)
  2807. response = requests.get(path, headers=self._headers())
  2808. self.assertEqual(http.OK, response.status_code)
  2809. tags = jsonutils.loads(response.text)['tags']
  2810. self.assertEqual(['gabe@example.com', 'sniff', 'snozz'],
  2811. sorted(tags))
  2812. # Query images by single tag
  2813. path = self._url('/v2/images?tag=sniff')
  2814. response = requests.get(path, headers=self._headers())
  2815. self.assertEqual(http.OK, response.status_code)
  2816. images = jsonutils.loads(response.text)['images']
  2817. self.assertEqual(1, len(images))
  2818. self.assertEqual('image-1', images[0]['name'])
  2819. # Query images by multiple tags
  2820. path = self._url('/v2/images?tag=sniff&tag=snozz')
  2821. response = requests.get(path, headers=self._headers())
  2822. self.assertEqual(http.OK, response.status_code)
  2823. images = jsonutils.loads(response.text)['images']
  2824. self.assertEqual(1, len(images))
  2825. self.assertEqual('image-1', images[0]['name'])
  2826. # Query images by tag and other attributes
  2827. path = self._url('/v2/images?tag=sniff&status=queued')
  2828. response = requests.get(path, headers=self._headers())
  2829. self.assertEqual(http.OK, response.status_code)
  2830. images = jsonutils.loads(response.text)['images']
  2831. self.assertEqual(1, len(images))
  2832. self.assertEqual('image-1', images[0]['name'])
  2833. # Query images by tag and a nonexistent tag
  2834. path = self._url('/v2/images?tag=sniff&tag=fake')
  2835. response = requests.get(path, headers=self._headers())
  2836. self.assertEqual(http.OK, response.status_code)
  2837. images = jsonutils.loads(response.text)['images']
  2838. self.assertEqual(0, len(images))
  2839. # The tag should be deletable
  2840. path = self._url('/v2/images/%s/tags/gabe%%40example.com' % image_id)
  2841. response = requests.delete(path, headers=self._headers())
  2842. self.assertEqual(http.NO_CONTENT, response.status_code)
  2843. # List of tags should reflect the deletion
  2844. path = self._url('/v2/images/%s' % image_id)
  2845. response = requests.get(path, headers=self._headers())
  2846. self.assertEqual(http.OK, response.status_code)
  2847. tags = jsonutils.loads(response.text)['tags']
  2848. self.assertEqual(['sniff', 'snozz'], sorted(tags))
  2849. # Deleting the same tag should return a 404
  2850. path = self._url('/v2/images/%s/tags/gabe%%40example.com' % image_id)
  2851. response = requests.delete(path, headers=self._headers())
  2852. self.assertEqual(http.NOT_FOUND, response.status_code)
  2853. # The tags won't be able to query the images after deleting
  2854. path = self._url('/v2/images?tag=gabe%%40example.com')
  2855. response = requests.get(path, headers=self._headers())
  2856. self.assertEqual(http.OK, response.status_code)
  2857. images = jsonutils.loads(response.text)['images']
  2858. self.assertEqual(0, len(images))
  2859. # Try to add a tag that is too long
  2860. big_tag = 'a' * 300
  2861. path = self._url('/v2/images/%s/tags/%s' % (image_id, big_tag))
  2862. response = requests.put(path, headers=self._headers())
  2863. self.assertEqual(http.BAD_REQUEST, response.status_code)
  2864. # Tags should not have changed since request was over limit
  2865. path = self._url('/v2/images/%s' % image_id)
  2866. response = requests.get(path, headers=self._headers())
  2867. self.assertEqual(http.OK, response.status_code)
  2868. tags = jsonutils.loads(response.text)['tags']
  2869. self.assertEqual(['sniff', 'snozz'], sorted(tags))
  2870. self.stop_servers()
  2871. def test_images_container(self):
  2872. # Image list should be empty and no next link should be present
  2873. self.start_servers(**self.__dict__.copy())
  2874. path = self._url('/v2/images')
  2875. response = requests.get(path, headers=self._headers())
  2876. self.assertEqual(http.OK, response.status_code)
  2877. images = jsonutils.loads(response.text)['images']
  2878. first = jsonutils.loads(response.text)['first']
  2879. self.assertEqual(0, len(images))
  2880. self.assertNotIn('next', jsonutils.loads(response.text))
  2881. self.assertEqual('/v2/images', first)
  2882. # Create 7 images
  2883. images = []
  2884. fixtures = [
  2885. {'name': 'image-3', 'type': 'kernel', 'ping': 'pong',
  2886. 'container_format': 'ami', 'disk_format': 'ami'},
  2887. {'name': 'image-4', 'type': 'kernel', 'ping': 'pong',
  2888. 'container_format': 'bare', 'disk_format': 'ami'},
  2889. {'name': 'image-1', 'type': 'kernel', 'ping': 'pong'},
  2890. {'name': 'image-3', 'type': 'ramdisk', 'ping': 'pong'},
  2891. {'name': 'image-2', 'type': 'kernel', 'ping': 'ding'},
  2892. {'name': 'image-3', 'type': 'kernel', 'ping': 'pong'},
  2893. {'name': 'image-2,image-5', 'type': 'kernel', 'ping': 'pong'},
  2894. ]
  2895. path = self._url('/v2/images')
  2896. headers = self._headers({'content-type': 'application/json'})
  2897. for fixture in fixtures:
  2898. data = jsonutils.dumps(fixture)
  2899. response = requests.post(path, headers=headers, data=data)
  2900. self.assertEqual(http.CREATED, response.status_code)
  2901. images.append(jsonutils.loads(response.text))
  2902. # Image list should contain 7 images
  2903. path = self._url('/v2/images')
  2904. response = requests.get(path, headers=self._headers())
  2905. self.assertEqual(http.OK, response.status_code)
  2906. body = jsonutils.loads(response.text)
  2907. self.assertEqual(7, len(body['images']))
  2908. self.assertEqual('/v2/images', body['first'])
  2909. self.assertNotIn('next', jsonutils.loads(response.text))
  2910. # Image list filters by created_at time
  2911. url_template = '/v2/images?created_at=lt:%s'
  2912. path = self._url(url_template % images[0]['created_at'])
  2913. response = requests.get(path, headers=self._headers())
  2914. self.assertEqual(http.OK, response.status_code)
  2915. body = jsonutils.loads(response.text)
  2916. self.assertEqual(0, len(body['images']))
  2917. self.assertEqual(url_template % images[0]['created_at'],
  2918. urllib.parse.unquote(body['first']))
  2919. # Image list filters by updated_at time
  2920. url_template = '/v2/images?updated_at=lt:%s'
  2921. path = self._url(url_template % images[2]['updated_at'])
  2922. response = requests.get(path, headers=self._headers())
  2923. self.assertEqual(http.OK, response.status_code)
  2924. body = jsonutils.loads(response.text)
  2925. self.assertGreaterEqual(3, len(body['images']))
  2926. self.assertEqual(url_template % images[2]['updated_at'],
  2927. urllib.parse.unquote(body['first']))
  2928. # Image list filters by updated_at and created time with invalid value
  2929. url_template = '/v2/images?%s=lt:invalid_value'
  2930. for filter in ['updated_at', 'created_at']:
  2931. path = self._url(url_template % filter)
  2932. response = requests.get(path, headers=self._headers())
  2933. self.assertEqual(http.BAD_REQUEST, response.status_code)
  2934. # Image list filters by updated_at and created_at with invalid operator
  2935. url_template = '/v2/images?%s=invalid_operator:2015-11-19T12:24:02Z'
  2936. for filter in ['updated_at', 'created_at']:
  2937. path = self._url(url_template % filter)
  2938. response = requests.get(path, headers=self._headers())
  2939. self.assertEqual(http.BAD_REQUEST, response.status_code)
  2940. # Image list filters by non-'URL encoding' value
  2941. path = self._url('/v2/images?name=%FF')
  2942. response = requests.get(path, headers=self._headers())
  2943. self.assertEqual(http.BAD_REQUEST, response.status_code)
  2944. # Image list filters by name with in operator
  2945. url_template = '/v2/images?name=in:%s'
  2946. filter_value = 'image-1,image-2'
  2947. path = self._url(url_template % filter_value)
  2948. response = requests.get(path, headers=self._headers())
  2949. self.assertEqual(http.OK, response.status_code)
  2950. body = jsonutils.loads(response.text)
  2951. self.assertGreaterEqual(3, len(body['images']))
  2952. # Image list filters by container_format with in operator
  2953. url_template = '/v2/images?container_format=in:%s'
  2954. filter_value = 'bare,ami'
  2955. path = self._url(url_template % filter_value)
  2956. response = requests.get(path, headers=self._headers())
  2957. self.assertEqual(http.OK, response.status_code)
  2958. body = jsonutils.loads(response.text)
  2959. self.assertGreaterEqual(2, len(body['images']))
  2960. # Image list filters by disk_format with in operator
  2961. url_template = '/v2/images?disk_format=in:%s'
  2962. filter_value = 'bare,ami,iso'
  2963. path = self._url(url_template % filter_value)
  2964. response = requests.get(path, headers=self._headers())
  2965. self.assertEqual(http.OK, response.status_code)
  2966. body = jsonutils.loads(response.text)
  2967. self.assertGreaterEqual(2, len(body['images']))
  2968. # Begin pagination after the first image
  2969. template_url = ('/v2/images?limit=2&sort_dir=asc&sort_key=name'
  2970. '&marker=%s&type=kernel&ping=pong')
  2971. path = self._url(template_url % images[2]['id'])
  2972. response = requests.get(path, headers=self._headers())
  2973. self.assertEqual(http.OK, response.status_code)
  2974. body = jsonutils.loads(response.text)
  2975. self.assertEqual(2, len(body['images']))
  2976. response_ids = [image['id'] for image in body['images']]
  2977. self.assertEqual([images[6]['id'], images[0]['id']], response_ids)
  2978. # Continue pagination using next link from previous request
  2979. path = self._url(body['next'])
  2980. response = requests.get(path, headers=self._headers())
  2981. self.assertEqual(http.OK, response.status_code)
  2982. body = jsonutils.loads(response.text)
  2983. self.assertEqual(2, len(body['images']))
  2984. response_ids = [image['id'] for image in body['images']]
  2985. self.assertEqual([images[5]['id'], images[1]['id']], response_ids)
  2986. # Continue pagination - expect no results
  2987. path = self._url(body['next'])
  2988. response = requests.get(path, headers=self._headers())
  2989. self.assertEqual(http.OK, response.status_code)
  2990. body = jsonutils.loads(response.text)
  2991. self.assertEqual(0, len(body['images']))
  2992. # Delete first image
  2993. path = self._url('/v2/images/%s' % images[0]['id'])
  2994. response = requests.delete(path, headers=self._headers())
  2995. self.assertEqual(http.NO_CONTENT, response.status_code)
  2996. # Ensure bad request for using a deleted image as marker
  2997. path = self._url('/v2/images?marker=%s' % images[0]['id'])
  2998. response = requests.get(path, headers=self._headers())
  2999. self.assertEqual(http.BAD_REQUEST, response.status_code)
  3000. self.stop_servers()
  3001. def test_image_visibility_to_different_users(self):
  3002. self.cleanup()
  3003. self.api_server.deployment_flavor = 'fakeauth'
  3004. self.registry_server.deployment_flavor = 'fakeauth'
  3005. kwargs = self.__dict__.copy()
  3006. kwargs['use_user_token'] = True
  3007. self.start_servers(**kwargs)
  3008. owners = ['admin', 'tenant1', 'tenant2', 'none']
  3009. visibilities = ['public', 'private', 'shared', 'community']
  3010. for owner in owners:
  3011. for visibility in visibilities:
  3012. path = self._url('/v2/images')
  3013. headers = self._headers({
  3014. 'content-type': 'application/json',
  3015. 'X-Auth-Token': 'createuser:%s:admin' % owner,
  3016. })
  3017. data = jsonutils.dumps({
  3018. 'name': '%s-%s' % (owner, visibility),
  3019. 'visibility': visibility,
  3020. })
  3021. response = requests.post(path, headers=headers, data=data)
  3022. self.assertEqual(http.CREATED, response.status_code)
  3023. def list_images(tenant, role='', visibility=None):
  3024. auth_token = 'user:%s:%s' % (tenant, role)
  3025. headers = {'X-Auth-Token': auth_token}
  3026. path = self._url('/v2/images')
  3027. if visibility is not None:
  3028. path += '?visibility=%s' % visibility
  3029. response = requests.get(path, headers=headers)
  3030. self.assertEqual(http.OK, response.status_code)
  3031. return jsonutils.loads(response.text)['images']
  3032. # 1. Known user sees public and their own images
  3033. images = list_images('tenant1')
  3034. self.assertEqual(7, len(images))
  3035. for image in images:
  3036. self.assertTrue(image['visibility'] == 'public'
  3037. or 'tenant1' in image['name'])
  3038. # 2. Known user, visibility=public, sees all public images
  3039. images = list_images('tenant1', visibility='public')
  3040. self.assertEqual(4, len(images))
  3041. for image in images:
  3042. self.assertEqual('public', image['visibility'])
  3043. # 3. Known user, visibility=private, sees only their private image
  3044. images = list_images('tenant1', visibility='private')
  3045. self.assertEqual(1, len(images))
  3046. image = images[0]
  3047. self.assertEqual('private', image['visibility'])
  3048. self.assertIn('tenant1', image['name'])
  3049. # 4. Known user, visibility=shared, sees only their shared image
  3050. images = list_images('tenant1', visibility='shared')
  3051. self.assertEqual(1, len(images))
  3052. image = images[0]
  3053. self.assertEqual('shared', image['visibility'])
  3054. self.assertIn('tenant1', image['name'])
  3055. # 5. Known user, visibility=community, sees all community images
  3056. images = list_images('tenant1', visibility='community')
  3057. self.assertEqual(4, len(images))
  3058. for image in images:
  3059. self.assertEqual('community', image['visibility'])
  3060. # 6. Unknown user sees only public images
  3061. images = list_images('none')
  3062. self.assertEqual(4, len(images))
  3063. for image in images:
  3064. self.assertEqual('public', image['visibility'])
  3065. # 7. Unknown user, visibility=public, sees only public images
  3066. images = list_images('none', visibility='public')
  3067. self.assertEqual(4, len(images))
  3068. for image in images:
  3069. self.assertEqual('public', image['visibility'])
  3070. # 8. Unknown user, visibility=private, sees no images
  3071. images = list_images('none', visibility='private')
  3072. self.assertEqual(0, len(images))
  3073. # 9. Unknown user, visibility=shared, sees no images
  3074. images = list_images('none', visibility='shared')
  3075. self.assertEqual(0, len(images))
  3076. # 10. Unknown user, visibility=community, sees only community images
  3077. images = list_images('none', visibility='community')
  3078. self.assertEqual(4, len(images))
  3079. for image in images:
  3080. self.assertEqual('community', image['visibility'])
  3081. # 11. Unknown admin sees all images except for community images
  3082. images = list_images('none', role='admin')
  3083. self.assertEqual(12, len(images))
  3084. # 12. Unknown admin, visibility=public, shows only public images
  3085. images = list_images('none', role='admin', visibility='public')
  3086. self.assertEqual(4, len(images))
  3087. for image in images:
  3088. self.assertEqual('public', image['visibility'])
  3089. # 13. Unknown admin, visibility=private, sees only private images
  3090. images = list_images('none', role='admin', visibility='private')
  3091. self.assertEqual(4, len(images))
  3092. for image in images:
  3093. self.assertEqual('private', image['visibility'])
  3094. # 14. Unknown admin, visibility=shared, sees only shared images
  3095. images = list_images('none', role='admin', visibility='shared')
  3096. self.assertEqual(4, len(images))
  3097. for image in images:
  3098. self.assertEqual('shared', image['visibility'])
  3099. # 15. Unknown admin, visibility=community, sees only community images
  3100. images = list_images('none', role='admin', visibility='community')
  3101. self.assertEqual(4, len(images))
  3102. for image in images:
  3103. self.assertEqual('community', image['visibility'])
  3104. # 16. Known admin sees all images, except community images owned by
  3105. # others
  3106. images = list_images('admin', role='admin')
  3107. self.assertEqual(13, len(images))
  3108. # 17. Known admin, visibility=public, sees all public images
  3109. images = list_images('admin', role='admin', visibility='public')
  3110. self.assertEqual(4, len(images))
  3111. for image in images:
  3112. self.assertEqual('public', image['visibility'])
  3113. # 18. Known admin, visibility=private, sees all private images
  3114. images = list_images('admin', role='admin', visibility='private')
  3115. self.assertEqual(4, len(images))
  3116. for image in images:
  3117. self.assertEqual('private', image['visibility'])
  3118. # 19. Known admin, visibility=shared, sees all shared images
  3119. images = list_images('admin', role='admin', visibility='shared')
  3120. self.assertEqual(4, len(images))
  3121. for image in images:
  3122. self.assertEqual('shared', image['visibility'])
  3123. # 20. Known admin, visibility=community, sees all community images
  3124. images = list_images('admin', role='admin', visibility='community')
  3125. self.assertEqual(4, len(images))
  3126. for image in images:
  3127. self.assertEqual('community', image['visibility'])
  3128. self.stop_servers()
  3129. def test_update_locations(self):
  3130. self.api_server.show_multiple_locations = True
  3131. self.start_servers(**self.__dict__.copy())
  3132. # Create an image
  3133. path = self._url('/v2/images')
  3134. headers = self._headers({'content-type': 'application/json'})
  3135. data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki',
  3136. 'container_format': 'aki'})
  3137. response = requests.post(path, headers=headers, data=data)
  3138. self.assertEqual(http.CREATED, response.status_code)
  3139. # Returned image entity should have a generated id and status
  3140. image = jsonutils.loads(response.text)
  3141. image_id = image['id']
  3142. self.assertEqual('queued', image['status'])
  3143. self.assertIsNone(image['size'])
  3144. self.assertIsNone(image['virtual_size'])
  3145. # Update locations for the queued image
  3146. path = self._url('/v2/images/%s' % image_id)
  3147. media_type = 'application/openstack-images-v2.1-json-patch'
  3148. headers = self._headers({'content-type': media_type})
  3149. url = 'http://127.0.0.1:%s/foo_image' % self.http_port0
  3150. data = jsonutils.dumps([{'op': 'replace', 'path': '/locations',
  3151. 'value': [{'url': url, 'metadata': {}}]
  3152. }])
  3153. response = requests.patch(path, headers=headers, data=data)
  3154. self.assertEqual(http.OK, response.status_code, response.text)
  3155. # The image size should be updated
  3156. path = self._url('/v2/images/%s' % image_id)
  3157. response = requests.get(path, headers=headers)
  3158. self.assertEqual(http.OK, response.status_code)
  3159. image = jsonutils.loads(response.text)
  3160. self.assertEqual(10, image['size'])
  3161. def test_update_locations_with_restricted_sources(self):
  3162. self.api_server.show_multiple_locations = True
  3163. self.start_servers(**self.__dict__.copy())
  3164. # Create an image
  3165. path = self._url('/v2/images')
  3166. headers = self._headers({'content-type': 'application/json'})
  3167. data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki',
  3168. 'container_format': 'aki'})
  3169. response = requests.post(path, headers=headers, data=data)
  3170. self.assertEqual(http.CREATED, response.status_code)
  3171. # Returned image entity should have a generated id and status
  3172. image = jsonutils.loads(response.text)
  3173. image_id = image['id']
  3174. self.assertEqual('queued', image['status'])
  3175. self.assertIsNone(image['size'])
  3176. self.assertIsNone(image['virtual_size'])
  3177. # Update locations for the queued image
  3178. path = self._url('/v2/images/%s' % image_id)
  3179. media_type = 'application/openstack-images-v2.1-json-patch'
  3180. headers = self._headers({'content-type': media_type})
  3181. data = jsonutils.dumps([{'op': 'replace', 'path': '/locations',
  3182. 'value': [{'url': 'file:///foo_image',
  3183. 'metadata': {}}]
  3184. }])
  3185. response = requests.patch(path, headers=headers, data=data)
  3186. self.assertEqual(http.BAD_REQUEST, response.status_code, response.text)
  3187. data = jsonutils.dumps([{'op': 'replace', 'path': '/locations',
  3188. 'value': [{'url': 'swift+config:///foo_image',
  3189. 'metadata': {}}]
  3190. }])
  3191. response = requests.patch(path, headers=headers, data=data)
  3192. self.assertEqual(http.BAD_REQUEST, response.status_code, response.text)
  3193. class TestImagesWithRegistry(TestImages):
  3194. def setUp(self):
  3195. super(TestImagesWithRegistry, self).setUp()
  3196. self.api_server.data_api = (
  3197. 'glance.tests.functional.v2.registry_data_api')
  3198. self.registry_server.deployment_flavor = 'trusted-auth'
  3199. self.api_server.use_user_token = True
  3200. class TestImagesIPv6(functional.FunctionalTest):
  3201. """Verify that API and REG servers running IPv6 can communicate"""
  3202. def setUp(self):
  3203. """
  3204. First applying monkey patches of functions and methods which have
  3205. IPv4 hardcoded.
  3206. """
  3207. # Setting up initial monkey patch (1)
  3208. test_utils.get_unused_port_ipv4 = test_utils.get_unused_port
  3209. test_utils.get_unused_port_and_socket_ipv4 = (
  3210. test_utils.get_unused_port_and_socket)
  3211. test_utils.get_unused_port = test_utils.get_unused_port_ipv6
  3212. test_utils.get_unused_port_and_socket = (
  3213. test_utils.get_unused_port_and_socket_ipv6)
  3214. super(TestImagesIPv6, self).setUp()
  3215. self.cleanup()
  3216. # Setting up monkey patch (2), after object is ready...
  3217. self.ping_server_ipv4 = self.ping_server
  3218. self.ping_server = self.ping_server_ipv6
  3219. self.include_scrubber = False
  3220. def tearDown(self):
  3221. # Cleaning up monkey patch (2).
  3222. self.ping_server = self.ping_server_ipv4
  3223. super(TestImagesIPv6, self).tearDown()
  3224. # Cleaning up monkey patch (1).
  3225. test_utils.get_unused_port = test_utils.get_unused_port_ipv4
  3226. test_utils.get_unused_port_and_socket = (
  3227. test_utils.get_unused_port_and_socket_ipv4)
  3228. def _url(self, path):
  3229. return "http://[::1]:%d%s" % (self.api_port, path)
  3230. def _headers(self, custom_headers=None):
  3231. base_headers = {
  3232. 'X-Identity-Status': 'Confirmed',
  3233. 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96',
  3234. 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e',
  3235. 'X-Tenant-Id': TENANT1,
  3236. 'X-Roles': 'member',
  3237. }
  3238. base_headers.update(custom_headers or {})
  3239. return base_headers
  3240. def test_image_list_ipv6(self):
  3241. # Image list should be empty
  3242. self.api_server.data_api = (
  3243. 'glance.tests.functional.v2.registry_data_api')
  3244. self.registry_server.deployment_flavor = 'trusted-auth'
  3245. # Setting up configuration parameters properly
  3246. # (bind_host is not needed since it is replaced by monkey patches,
  3247. # but it would be reflected in the configuration file, which is
  3248. # at least improving consistency)
  3249. self.registry_server.bind_host = "::1"
  3250. self.api_server.bind_host = "::1"
  3251. self.api_server.registry_host = "::1"
  3252. self.scrubber_daemon.registry_host = "::1"
  3253. self.start_servers(**self.__dict__.copy())
  3254. requests.get(self._url('/'), headers=self._headers())
  3255. path = self._url('/v2/images')
  3256. response = requests.get(path, headers=self._headers())
  3257. self.assertEqual(200, response.status_code)
  3258. images = jsonutils.loads(response.text)['images']
  3259. self.assertEqual(0, len(images))
  3260. class TestImageDirectURLVisibility(functional.FunctionalTest):
  3261. def setUp(self):
  3262. super(TestImageDirectURLVisibility, self).setUp()
  3263. self.cleanup()
  3264. self.include_scrubber = False
  3265. self.api_server.deployment_flavor = 'noauth'
  3266. def _url(self, path):
  3267. return 'http://127.0.0.1:%d%s' % (self.api_port, path)
  3268. def _headers(self, custom_headers=None):
  3269. base_headers = {
  3270. 'X-Identity-Status': 'Confirmed',
  3271. 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96',
  3272. 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e',
  3273. 'X-Tenant-Id': TENANT1,
  3274. 'X-Roles': 'member',
  3275. }
  3276. base_headers.update(custom_headers or {})
  3277. return base_headers
  3278. def test_v2_not_enabled(self):
  3279. self.api_server.enable_v2_api = False
  3280. self.start_servers(**self.__dict__.copy())
  3281. path = self._url('/v2/images')
  3282. response = requests.get(path, headers=self._headers())
  3283. self.assertEqual(http.MULTIPLE_CHOICES, response.status_code)
  3284. self.stop_servers()
  3285. def test_v2_enabled(self):
  3286. self.api_server.enable_v2_api = True
  3287. self.start_servers(**self.__dict__.copy())
  3288. path = self._url('/v2/images')
  3289. response = requests.get(path, headers=self._headers())
  3290. self.assertEqual(http.OK, response.status_code)
  3291. self.stop_servers()
  3292. def test_image_direct_url_visible(self):
  3293. self.api_server.show_image_direct_url = True
  3294. self.start_servers(**self.__dict__.copy())
  3295. # Image list should be empty
  3296. path = self._url('/v2/images')
  3297. response = requests.get(path, headers=self._headers())
  3298. self.assertEqual(http.OK, response.status_code)
  3299. images = jsonutils.loads(response.text)['images']
  3300. self.assertEqual(0, len(images))
  3301. # Create an image
  3302. path = self._url('/v2/images')
  3303. headers = self._headers({'content-type': 'application/json'})
  3304. data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel',
  3305. 'foo': 'bar', 'disk_format': 'aki',
  3306. 'container_format': 'aki',
  3307. 'visibility': 'public'})
  3308. response = requests.post(path, headers=headers, data=data)
  3309. self.assertEqual(http.CREATED, response.status_code)
  3310. # Get the image id
  3311. image = jsonutils.loads(response.text)
  3312. image_id = image['id']
  3313. # Image direct_url should not be visible before location is set
  3314. path = self._url('/v2/images/%s' % image_id)
  3315. headers = self._headers({'Content-Type': 'application/json'})
  3316. response = requests.get(path, headers=headers)
  3317. self.assertEqual(http.OK, response.status_code)
  3318. image = jsonutils.loads(response.text)
  3319. self.assertNotIn('direct_url', image)
  3320. # Upload some image data, setting the image location
  3321. path = self._url('/v2/images/%s/file' % image_id)
  3322. headers = self._headers({'Content-Type': 'application/octet-stream'})
  3323. response = requests.put(path, headers=headers, data='ZZZZZ')
  3324. self.assertEqual(http.NO_CONTENT, response.status_code)
  3325. # Image direct_url should be visible
  3326. path = self._url('/v2/images/%s' % image_id)
  3327. headers = self._headers({'Content-Type': 'application/json'})
  3328. response = requests.get(path, headers=headers)
  3329. self.assertEqual(http.OK, response.status_code)
  3330. image = jsonutils.loads(response.text)
  3331. self.assertIn('direct_url', image)
  3332. # Image direct_url should be visible to non-owner, non-admin user
  3333. path = self._url('/v2/images/%s' % image_id)
  3334. headers = self._headers({'Content-Type': 'application/json',
  3335. 'X-Tenant-Id': TENANT2})
  3336. response = requests.get(path, headers=headers)
  3337. self.assertEqual(http.OK, response.status_code)
  3338. image = jsonutils.loads(response.text)
  3339. self.assertIn('direct_url', image)
  3340. # Image direct_url should be visible in a list
  3341. path = self._url('/v2/images')
  3342. headers = self._headers({'Content-Type': 'application/json'})
  3343. response = requests.get(path, headers=headers)
  3344. self.assertEqual(http.OK, response.status_code)
  3345. image = jsonutils.loads(response.text)['images'][0]
  3346. self.assertIn('direct_url', image)
  3347. self.stop_servers()
  3348. def test_image_multiple_location_url_visible(self):
  3349. self.api_server.show_multiple_locations = True
  3350. self.start_servers(**self.__dict__.copy())
  3351. # Create an image
  3352. path = self._url('/v2/images')
  3353. headers = self._headers({'content-type': 'application/json'})
  3354. data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel',
  3355. 'foo': 'bar', 'disk_format': 'aki',
  3356. 'container_format': 'aki'})
  3357. response = requests.post(path, headers=headers, data=data)
  3358. self.assertEqual(http.CREATED, response.status_code)
  3359. # Get the image id
  3360. image = jsonutils.loads(response.text)
  3361. image_id = image['id']
  3362. # Image locations should not be visible before location is set
  3363. path = self._url('/v2/images/%s' % image_id)
  3364. headers = self._headers({'Content-Type': 'application/json'})
  3365. response = requests.get(path, headers=headers)
  3366. self.assertEqual(http.OK, response.status_code)
  3367. image = jsonutils.loads(response.text)
  3368. self.assertIn('locations', image)
  3369. self.assertEqual([], image["locations"])
  3370. # Upload some image data, setting the image location
  3371. path = self._url('/v2/images/%s/file' % image_id)
  3372. headers = self._headers({'Content-Type': 'application/octet-stream'})
  3373. response = requests.put(path, headers=headers, data='ZZZZZ')
  3374. self.assertEqual(http.NO_CONTENT, response.status_code)
  3375. # Image locations should be visible
  3376. path = self._url('/v2/images/%s' % image_id)
  3377. headers = self._headers({'Content-Type': 'application/json'})
  3378. response = requests.get(path, headers=headers)
  3379. self.assertEqual(http.OK, response.status_code)
  3380. image = jsonutils.loads(response.text)
  3381. self.assertIn('locations', image)
  3382. loc = image['locations']
  3383. self.assertGreater(len(loc), 0)
  3384. loc = loc[0]
  3385. self.assertIn('url', loc)
  3386. self.assertIn('metadata', loc)
  3387. self.stop_servers()
  3388. def test_image_direct_url_not_visible(self):
  3389. self.api_server.show_image_direct_url = False
  3390. self.start_servers(**self.__dict__.copy())
  3391. # Image list should be empty
  3392. path = self._url('/v2/images')
  3393. response = requests.get(path, headers=self._headers())
  3394. self.assertEqual(http.OK, response.status_code)
  3395. images = jsonutils.loads(response.text)['images']
  3396. self.assertEqual(0, len(images))
  3397. # Create an image
  3398. path = self._url('/v2/images')
  3399. headers = self._headers({'content-type': 'application/json'})
  3400. data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel',
  3401. 'foo': 'bar', 'disk_format': 'aki',
  3402. 'container_format': 'aki'})
  3403. response = requests.post(path, headers=headers, data=data)
  3404. self.assertEqual(http.CREATED, response.status_code)
  3405. # Get the image id
  3406. image = jsonutils.loads(response.text)
  3407. image_id = image['id']
  3408. # Upload some image data, setting the image location
  3409. path = self._url('/v2/images/%s/file' % image_id)
  3410. headers = self._headers({'Content-Type': 'application/octet-stream'})
  3411. response = requests.put(path, headers=headers, data='ZZZZZ')
  3412. self.assertEqual(http.NO_CONTENT, response.status_code)
  3413. # Image direct_url should not be visible
  3414. path = self._url('/v2/images/%s' % image_id)
  3415. headers = self._headers({'Content-Type': 'application/json'})
  3416. response = requests.get(path, headers=headers)
  3417. self.assertEqual(http.OK, response.status_code)
  3418. image = jsonutils.loads(response.text)
  3419. self.assertNotIn('direct_url', image)
  3420. # Image direct_url should not be visible in a list
  3421. path = self._url('/v2/images')
  3422. headers = self._headers({'Content-Type': 'application/json'})
  3423. response = requests.get(path, headers=headers)
  3424. self.assertEqual(http.OK, response.status_code)
  3425. image = jsonutils.loads(response.text)['images'][0]
  3426. self.assertNotIn('direct_url', image)
  3427. self.stop_servers()
  3428. class TestImageDirectURLVisibilityWithRegistry(TestImageDirectURLVisibility):
  3429. def setUp(self):
  3430. super(TestImageDirectURLVisibilityWithRegistry, self).setUp()
  3431. self.api_server.data_api = (
  3432. 'glance.tests.functional.v2.registry_data_api')
  3433. self.registry_server.deployment_flavor = 'trusted-auth'
  3434. class TestImageLocationSelectionStrategy(functional.FunctionalTest):
  3435. def setUp(self):
  3436. super(TestImageLocationSelectionStrategy, self).setUp()
  3437. self.cleanup()
  3438. self.include_scrubber = False
  3439. self.api_server.deployment_flavor = 'noauth'
  3440. for i in range(3):
  3441. ret = test_utils.start_http_server("foo_image_id%d" % i,
  3442. "foo_image%d" % i)
  3443. setattr(self, 'http_server%d_pid' % i, ret[0])
  3444. setattr(self, 'http_port%d' % i, ret[1])
  3445. def tearDown(self):
  3446. for i in range(3):
  3447. pid = getattr(self, 'http_server%d_pid' % i, None)
  3448. if pid:
  3449. os.kill(pid, signal.SIGKILL)
  3450. super(TestImageLocationSelectionStrategy, self).tearDown()
  3451. def _url(self, path):
  3452. return 'http://127.0.0.1:%d%s' % (self.api_port, path)
  3453. def _headers(self, custom_headers=None):
  3454. base_headers = {
  3455. 'X-Identity-Status': 'Confirmed',
  3456. 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96',
  3457. 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e',
  3458. 'X-Tenant-Id': TENANT1,
  3459. 'X-Roles': 'member',
  3460. }
  3461. base_headers.update(custom_headers or {})
  3462. return base_headers
  3463. def test_image_locations_with_order_strategy(self):
  3464. self.api_server.show_image_direct_url = True
  3465. self.api_server.show_multiple_locations = True
  3466. self.image_location_quota = 10
  3467. self.api_server.location_strategy = 'location_order'
  3468. preference = "http, swift, filesystem"
  3469. self.api_server.store_type_location_strategy_preference = preference
  3470. self.start_servers(**self.__dict__.copy())
  3471. # Create an image
  3472. path = self._url('/v2/images')
  3473. headers = self._headers({'content-type': 'application/json'})
  3474. data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel',
  3475. 'foo': 'bar', 'disk_format': 'aki',
  3476. 'container_format': 'aki'})
  3477. response = requests.post(path, headers=headers, data=data)
  3478. self.assertEqual(http.CREATED, response.status_code)
  3479. # Get the image id
  3480. image = jsonutils.loads(response.text)
  3481. image_id = image['id']
  3482. # Image locations should not be visible before location is set
  3483. path = self._url('/v2/images/%s' % image_id)
  3484. headers = self._headers({'Content-Type': 'application/json'})
  3485. response = requests.get(path, headers=headers)
  3486. self.assertEqual(http.OK, response.status_code)
  3487. image = jsonutils.loads(response.text)
  3488. self.assertIn('locations', image)
  3489. self.assertEqual([], image["locations"])
  3490. # Update image locations via PATCH
  3491. path = self._url('/v2/images/%s' % image_id)
  3492. media_type = 'application/openstack-images-v2.1-json-patch'
  3493. headers = self._headers({'content-type': media_type})
  3494. values = [{'url': 'http://127.0.0.1:%s/foo_image' % self.http_port0,
  3495. 'metadata': {}},
  3496. {'url': 'http://127.0.0.1:%s/foo_image' % self.http_port1,
  3497. 'metadata': {}}]
  3498. doc = [{'op': 'replace',
  3499. 'path': '/locations',
  3500. 'value': values}]
  3501. data = jsonutils.dumps(doc)
  3502. response = requests.patch(path, headers=headers, data=data)
  3503. self.assertEqual(http.OK, response.status_code)
  3504. # Image locations should be visible
  3505. path = self._url('/v2/images/%s' % image_id)
  3506. headers = self._headers({'Content-Type': 'application/json'})
  3507. response = requests.get(path, headers=headers)
  3508. self.assertEqual(http.OK, response.status_code)
  3509. image = jsonutils.loads(response.text)
  3510. self.assertIn('locations', image)
  3511. self.assertEqual(values, image['locations'])
  3512. self.assertIn('direct_url', image)
  3513. self.assertEqual(values[0]['url'], image['direct_url'])
  3514. self.stop_servers()
  3515. class TestImageLocationSelectionStrategyWithRegistry(
  3516. TestImageLocationSelectionStrategy):
  3517. def setUp(self):
  3518. super(TestImageLocationSelectionStrategyWithRegistry, self).setUp()
  3519. self.api_server.data_api = (
  3520. 'glance.tests.functional.v2.registry_data_api')
  3521. self.registry_server.deployment_flavor = 'trusted-auth'
  3522. class TestImageMembers(functional.FunctionalTest):
  3523. def setUp(self):
  3524. super(TestImageMembers, self).setUp()
  3525. self.cleanup()
  3526. self.include_scrubber = False
  3527. self.api_server.deployment_flavor = 'fakeauth'
  3528. self.registry_server.deployment_flavor = 'fakeauth'
  3529. self.start_servers(**self.__dict__.copy())
  3530. def _url(self, path):
  3531. return 'http://127.0.0.1:%d%s' % (self.api_port, path)
  3532. def _headers(self, custom_headers=None):
  3533. base_headers = {
  3534. 'X-Identity-Status': 'Confirmed',
  3535. 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96',
  3536. 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e',
  3537. 'X-Tenant-Id': TENANT1,
  3538. 'X-Roles': 'member',
  3539. }
  3540. base_headers.update(custom_headers or {})
  3541. return base_headers
  3542. def test_image_member_lifecycle(self):
  3543. def get_header(tenant, role=''):
  3544. auth_token = 'user:%s:%s' % (tenant, role)
  3545. headers = {'X-Auth-Token': auth_token}
  3546. return headers
  3547. # Image list should be empty
  3548. path = self._url('/v2/images')
  3549. response = requests.get(path, headers=get_header('tenant1'))
  3550. self.assertEqual(http.OK, response.status_code)
  3551. images = jsonutils.loads(response.text)['images']
  3552. self.assertEqual(0, len(images))
  3553. owners = ['tenant1', 'tenant2', 'admin']
  3554. visibilities = ['community', 'private', 'public', 'shared']
  3555. image_fixture = []
  3556. for owner in owners:
  3557. for visibility in visibilities:
  3558. path = self._url('/v2/images')
  3559. headers = self._headers({
  3560. 'content-type': 'application/json',
  3561. 'X-Auth-Token': 'createuser:%s:admin' % owner,
  3562. })
  3563. data = jsonutils.dumps({
  3564. 'name': '%s-%s' % (owner, visibility),
  3565. 'visibility': visibility,
  3566. })
  3567. response = requests.post(path, headers=headers, data=data)
  3568. self.assertEqual(http.CREATED, response.status_code)
  3569. image_fixture.append(jsonutils.loads(response.text))
  3570. # Image list should contain 6 images for tenant1
  3571. path = self._url('/v2/images')
  3572. response = requests.get(path, headers=get_header('tenant1'))
  3573. self.assertEqual(http.OK, response.status_code)
  3574. images = jsonutils.loads(response.text)['images']
  3575. self.assertEqual(6, len(images))
  3576. # Image list should contain 3 images for TENANT3
  3577. path = self._url('/v2/images')
  3578. response = requests.get(path, headers=get_header(TENANT3))
  3579. self.assertEqual(http.OK, response.status_code)
  3580. images = jsonutils.loads(response.text)['images']
  3581. self.assertEqual(3, len(images))
  3582. # Add Image member for tenant1-shared image
  3583. path = self._url('/v2/images/%s/members' % image_fixture[3]['id'])
  3584. body = jsonutils.dumps({'member': TENANT3})
  3585. response = requests.post(path, headers=get_header('tenant1'),
  3586. data=body)
  3587. self.assertEqual(http.OK, response.status_code)
  3588. image_member = jsonutils.loads(response.text)
  3589. self.assertEqual(image_fixture[3]['id'], image_member['image_id'])
  3590. self.assertEqual(TENANT3, image_member['member_id'])
  3591. self.assertIn('created_at', image_member)
  3592. self.assertIn('updated_at', image_member)
  3593. self.assertEqual('pending', image_member['status'])
  3594. # Image list should contain 3 images for TENANT3
  3595. path = self._url('/v2/images')
  3596. response = requests.get(path, headers=get_header(TENANT3))
  3597. self.assertEqual(http.OK, response.status_code)
  3598. images = jsonutils.loads(response.text)['images']
  3599. self.assertEqual(3, len(images))
  3600. # Image list should contain 0 shared images for TENANT3
  3601. # because default is accepted
  3602. path = self._url('/v2/images?visibility=shared')
  3603. response = requests.get(path, headers=get_header(TENANT3))
  3604. self.assertEqual(http.OK, response.status_code)
  3605. images = jsonutils.loads(response.text)['images']
  3606. self.assertEqual(0, len(images))
  3607. # Image list should contain 4 images for TENANT3 with status pending
  3608. path = self._url('/v2/images?member_status=pending')
  3609. response = requests.get(path, headers=get_header(TENANT3))
  3610. self.assertEqual(http.OK, response.status_code)
  3611. images = jsonutils.loads(response.text)['images']
  3612. self.assertEqual(4, len(images))
  3613. # Image list should contain 4 images for TENANT3 with status all
  3614. path = self._url('/v2/images?member_status=all')
  3615. response = requests.get(path, headers=get_header(TENANT3))
  3616. self.assertEqual(http.OK, response.status_code)
  3617. images = jsonutils.loads(response.text)['images']
  3618. self.assertEqual(4, len(images))
  3619. # Image list should contain 1 image for TENANT3 with status pending
  3620. # and visibility shared
  3621. path = self._url('/v2/images?member_status=pending&visibility=shared')
  3622. response = requests.get(path, headers=get_header(TENANT3))
  3623. self.assertEqual(http.OK, response.status_code)
  3624. images = jsonutils.loads(response.text)['images']
  3625. self.assertEqual(1, len(images))
  3626. self.assertEqual(images[0]['name'], 'tenant1-shared')
  3627. # Image list should contain 0 image for TENANT3 with status rejected
  3628. # and visibility shared
  3629. path = self._url('/v2/images?member_status=rejected&visibility=shared')
  3630. response = requests.get(path, headers=get_header(TENANT3))
  3631. self.assertEqual(http.OK, response.status_code)
  3632. images = jsonutils.loads(response.text)['images']
  3633. self.assertEqual(0, len(images))
  3634. # Image list should contain 0 image for TENANT3 with status accepted
  3635. # and visibility shared
  3636. path = self._url('/v2/images?member_status=accepted&visibility=shared')
  3637. response = requests.get(path, headers=get_header(TENANT3))
  3638. self.assertEqual(http.OK, response.status_code)
  3639. images = jsonutils.loads(response.text)['images']
  3640. self.assertEqual(0, len(images))
  3641. # Image list should contain 0 image for TENANT3 with status accepted
  3642. # and visibility private
  3643. path = self._url('/v2/images?visibility=private')
  3644. response = requests.get(path, headers=get_header(TENANT3))
  3645. self.assertEqual(http.OK, response.status_code)
  3646. images = jsonutils.loads(response.text)['images']
  3647. self.assertEqual(0, len(images))
  3648. # Image tenant2-shared's image members list should contain no members
  3649. path = self._url('/v2/images/%s/members' % image_fixture[7]['id'])
  3650. response = requests.get(path, headers=get_header('tenant2'))
  3651. self.assertEqual(http.OK, response.status_code)
  3652. body = jsonutils.loads(response.text)
  3653. self.assertEqual(0, len(body['members']))
  3654. # Tenant 1, who is the owner cannot change status of image member
  3655. path = self._url('/v2/images/%s/members/%s' % (image_fixture[3]['id'],
  3656. TENANT3))
  3657. body = jsonutils.dumps({'status': 'accepted'})
  3658. response = requests.put(path, headers=get_header('tenant1'), data=body)
  3659. self.assertEqual(http.FORBIDDEN, response.status_code)
  3660. # Tenant 1, who is the owner can get status of its own image member
  3661. path = self._url('/v2/images/%s/members/%s' % (image_fixture[3]['id'],
  3662. TENANT3))
  3663. response = requests.get(path, headers=get_header('tenant1'))
  3664. self.assertEqual(http.OK, response.status_code)
  3665. body = jsonutils.loads(response.text)
  3666. self.assertEqual('pending', body['status'])
  3667. self.assertEqual(image_fixture[3]['id'], body['image_id'])
  3668. self.assertEqual(TENANT3, body['member_id'])
  3669. # Tenant 3, who is the member can get status of its own status
  3670. path = self._url('/v2/images/%s/members/%s' % (image_fixture[3]['id'],
  3671. TENANT3))
  3672. response = requests.get(path, headers=get_header(TENANT3))
  3673. self.assertEqual(http.OK, response.status_code)
  3674. body = jsonutils.loads(response.text)
  3675. self.assertEqual('pending', body['status'])
  3676. self.assertEqual(image_fixture[3]['id'], body['image_id'])
  3677. self.assertEqual(TENANT3, body['member_id'])
  3678. # Tenant 2, who not the owner cannot get status of image member
  3679. path = self._url('/v2/images/%s/members/%s' % (image_fixture[3]['id'],
  3680. TENANT3))
  3681. response = requests.get(path, headers=get_header('tenant2'))
  3682. self.assertEqual(http.NOT_FOUND, response.status_code)
  3683. # Tenant 3 can change status of image member
  3684. path = self._url('/v2/images/%s/members/%s' % (image_fixture[3]['id'],
  3685. TENANT3))
  3686. body = jsonutils.dumps({'status': 'accepted'})
  3687. response = requests.put(path, headers=get_header(TENANT3), data=body)
  3688. self.assertEqual(http.OK, response.status_code)
  3689. image_member = jsonutils.loads(response.text)
  3690. self.assertEqual(image_fixture[3]['id'], image_member['image_id'])
  3691. self.assertEqual(TENANT3, image_member['member_id'])
  3692. self.assertEqual('accepted', image_member['status'])
  3693. # Image list should contain 4 images for TENANT3 because status is
  3694. # accepted
  3695. path = self._url('/v2/images')
  3696. response = requests.get(path, headers=get_header(TENANT3))
  3697. self.assertEqual(http.OK, response.status_code)
  3698. images = jsonutils.loads(response.text)['images']
  3699. self.assertEqual(4, len(images))
  3700. # Tenant 3 invalid status change
  3701. path = self._url('/v2/images/%s/members/%s' % (image_fixture[3]['id'],
  3702. TENANT3))
  3703. body = jsonutils.dumps({'status': 'invalid-status'})
  3704. response = requests.put(path, headers=get_header(TENANT3), data=body)
  3705. self.assertEqual(http.BAD_REQUEST, response.status_code)
  3706. # Owner cannot change status of image
  3707. path = self._url('/v2/images/%s/members/%s' % (image_fixture[3]['id'],
  3708. TENANT3))
  3709. body = jsonutils.dumps({'status': 'accepted'})
  3710. response = requests.put(path, headers=get_header('tenant1'), data=body)
  3711. self.assertEqual(http.FORBIDDEN, response.status_code)
  3712. # Add Image member for tenant2-shared image
  3713. path = self._url('/v2/images/%s/members' % image_fixture[7]['id'])
  3714. body = jsonutils.dumps({'member': TENANT4})
  3715. response = requests.post(path, headers=get_header('tenant2'),
  3716. data=body)
  3717. self.assertEqual(http.OK, response.status_code)
  3718. image_member = jsonutils.loads(response.text)
  3719. self.assertEqual(image_fixture[7]['id'], image_member['image_id'])
  3720. self.assertEqual(TENANT4, image_member['member_id'])
  3721. self.assertIn('created_at', image_member)
  3722. self.assertIn('updated_at', image_member)
  3723. # Add Image member to public image
  3724. path = self._url('/v2/images/%s/members' % image_fixture[2]['id'])
  3725. body = jsonutils.dumps({'member': TENANT2})
  3726. response = requests.post(path, headers=get_header('tenant1'),
  3727. data=body)
  3728. self.assertEqual(http.FORBIDDEN, response.status_code)
  3729. # Add Image member to private image
  3730. path = self._url('/v2/images/%s/members' % image_fixture[1]['id'])
  3731. body = jsonutils.dumps({'member': TENANT2})
  3732. response = requests.post(path, headers=get_header('tenant1'),
  3733. data=body)
  3734. self.assertEqual(http.FORBIDDEN, response.status_code)
  3735. # Add Image member to community image
  3736. path = self._url('/v2/images/%s/members' % image_fixture[0]['id'])
  3737. body = jsonutils.dumps({'member': TENANT2})
  3738. response = requests.post(path, headers=get_header('tenant1'),
  3739. data=body)
  3740. self.assertEqual(http.FORBIDDEN, response.status_code)
  3741. # Image tenant1-shared's members list should contain 1 member
  3742. path = self._url('/v2/images/%s/members' % image_fixture[3]['id'])
  3743. response = requests.get(path, headers=get_header('tenant1'))
  3744. self.assertEqual(http.OK, response.status_code)
  3745. body = jsonutils.loads(response.text)
  3746. self.assertEqual(1, len(body['members']))
  3747. # Admin can see any members
  3748. path = self._url('/v2/images/%s/members' % image_fixture[3]['id'])
  3749. response = requests.get(path, headers=get_header('tenant1', 'admin'))
  3750. self.assertEqual(http.OK, response.status_code)
  3751. body = jsonutils.loads(response.text)
  3752. self.assertEqual(1, len(body['members']))
  3753. # Image members not found for private image not owned by TENANT 1
  3754. path = self._url('/v2/images/%s/members' % image_fixture[7]['id'])
  3755. response = requests.get(path, headers=get_header('tenant1'))
  3756. self.assertEqual(http.NOT_FOUND, response.status_code)
  3757. # Image members forbidden for public image
  3758. path = self._url('/v2/images/%s/members' % image_fixture[2]['id'])
  3759. response = requests.get(path, headers=get_header('tenant1'))
  3760. self.assertIn("Only shared images have members", response.text)
  3761. self.assertEqual(http.FORBIDDEN, response.status_code)
  3762. # Image members forbidden for community image
  3763. path = self._url('/v2/images/%s/members' % image_fixture[0]['id'])
  3764. response = requests.get(path, headers=get_header('tenant1'))
  3765. self.assertIn("Only shared images have members", response.text)
  3766. self.assertEqual(http.FORBIDDEN, response.status_code)
  3767. # Image members forbidden for private image
  3768. path = self._url('/v2/images/%s/members' % image_fixture[1]['id'])
  3769. response = requests.get(path, headers=get_header('tenant1'))
  3770. self.assertIn("Only shared images have members", response.text)
  3771. self.assertEqual(http.FORBIDDEN, response.status_code)
  3772. # Image Member Cannot delete Image membership
  3773. path = self._url('/v2/images/%s/members/%s' % (image_fixture[3]['id'],
  3774. TENANT3))
  3775. response = requests.delete(path, headers=get_header(TENANT3))
  3776. self.assertEqual(http.FORBIDDEN, response.status_code)
  3777. # Delete Image member
  3778. path = self._url('/v2/images/%s/members/%s' % (image_fixture[3]['id'],
  3779. TENANT3))
  3780. response = requests.delete(path, headers=get_header('tenant1'))
  3781. self.assertEqual(http.NO_CONTENT, response.status_code)
  3782. # Now the image has no members
  3783. path = self._url('/v2/images/%s/members' % image_fixture[3]['id'])
  3784. response = requests.get(path, headers=get_header('tenant1'))
  3785. self.assertEqual(http.OK, response.status_code)
  3786. body = jsonutils.loads(response.text)
  3787. self.assertEqual(0, len(body['members']))
  3788. # Adding 11 image members should fail since configured limit is 10
  3789. path = self._url('/v2/images/%s/members' % image_fixture[3]['id'])
  3790. for i in range(10):
  3791. body = jsonutils.dumps({'member': str(uuid.uuid4())})
  3792. response = requests.post(path, headers=get_header('tenant1'),
  3793. data=body)
  3794. self.assertEqual(http.OK, response.status_code)
  3795. body = jsonutils.dumps({'member': str(uuid.uuid4())})
  3796. response = requests.post(path, headers=get_header('tenant1'),
  3797. data=body)
  3798. self.assertEqual(http.REQUEST_ENTITY_TOO_LARGE, response.status_code)
  3799. # Get Image member should return not found for public image
  3800. path = self._url('/v2/images/%s/members/%s' % (image_fixture[2]['id'],
  3801. TENANT3))
  3802. response = requests.get(path, headers=get_header('tenant1'))
  3803. self.assertEqual(http.NOT_FOUND, response.status_code)
  3804. # Get Image member should return not found for community image
  3805. path = self._url('/v2/images/%s/members/%s' % (image_fixture[0]['id'],
  3806. TENANT3))
  3807. response = requests.get(path, headers=get_header('tenant1'))
  3808. self.assertEqual(http.NOT_FOUND, response.status_code)
  3809. # Get Image member should return not found for private image
  3810. path = self._url('/v2/images/%s/members/%s' % (image_fixture[1]['id'],
  3811. TENANT3))
  3812. response = requests.get(path, headers=get_header('tenant1'))
  3813. self.assertEqual(http.NOT_FOUND, response.status_code)
  3814. # Delete Image member should return forbidden for public image
  3815. path = self._url('/v2/images/%s/members/%s' % (image_fixture[2]['id'],
  3816. TENANT3))
  3817. response = requests.delete(path, headers=get_header('tenant1'))
  3818. self.assertEqual(http.FORBIDDEN, response.status_code)
  3819. # Delete Image member should return forbidden for community image
  3820. path = self._url('/v2/images/%s/members/%s' % (image_fixture[0]['id'],
  3821. TENANT3))
  3822. response = requests.delete(path, headers=get_header('tenant1'))
  3823. self.assertEqual(http.FORBIDDEN, response.status_code)
  3824. # Delete Image member should return forbidden for private image
  3825. path = self._url('/v2/images/%s/members/%s' % (image_fixture[1]['id'],
  3826. TENANT3))
  3827. response = requests.delete(path, headers=get_header('tenant1'))
  3828. self.assertEqual(http.FORBIDDEN, response.status_code)
  3829. self.stop_servers()
  3830. class TestImageMembersWithRegistry(TestImageMembers):
  3831. def setUp(self):
  3832. super(TestImageMembersWithRegistry, self).setUp()
  3833. self.api_server.data_api = (
  3834. 'glance.tests.functional.v2.registry_data_api')
  3835. self.registry_server.deployment_flavor = 'trusted-auth'
  3836. class TestQuotas(functional.FunctionalTest):
  3837. def setUp(self):
  3838. super(TestQuotas, self).setUp()
  3839. self.cleanup()
  3840. self.include_scrubber = False
  3841. self.api_server.deployment_flavor = 'noauth'
  3842. self.registry_server.deployment_flavor = 'trusted-auth'
  3843. self.user_storage_quota = 100
  3844. self.start_servers(**self.__dict__.copy())
  3845. def _url(self, path):
  3846. return 'http://127.0.0.1:%d%s' % (self.api_port, path)
  3847. def _headers(self, custom_headers=None):
  3848. base_headers = {
  3849. 'X-Identity-Status': 'Confirmed',
  3850. 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96',
  3851. 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e',
  3852. 'X-Tenant-Id': TENANT1,
  3853. 'X-Roles': 'member',
  3854. }
  3855. base_headers.update(custom_headers or {})
  3856. return base_headers
  3857. def _upload_image_test(self, data_src, expected_status):
  3858. # Image list should be empty
  3859. path = self._url('/v2/images')
  3860. response = requests.get(path, headers=self._headers())
  3861. self.assertEqual(http.OK, response.status_code)
  3862. images = jsonutils.loads(response.text)['images']
  3863. self.assertEqual(0, len(images))
  3864. # Create an image (with a deployer-defined property)
  3865. path = self._url('/v2/images')
  3866. headers = self._headers({'content-type': 'application/json'})
  3867. data = jsonutils.dumps({'name': 'testimg',
  3868. 'type': 'kernel',
  3869. 'foo': 'bar',
  3870. 'disk_format': 'aki',
  3871. 'container_format': 'aki'})
  3872. response = requests.post(path, headers=headers, data=data)
  3873. self.assertEqual(http.CREATED, response.status_code)
  3874. image = jsonutils.loads(response.text)
  3875. image_id = image['id']
  3876. # upload data
  3877. path = self._url('/v2/images/%s/file' % image_id)
  3878. headers = self._headers({'Content-Type': 'application/octet-stream'})
  3879. response = requests.put(path, headers=headers, data=data_src)
  3880. self.assertEqual(expected_status, response.status_code)
  3881. # Deletion should work
  3882. path = self._url('/v2/images/%s' % image_id)
  3883. response = requests.delete(path, headers=self._headers())
  3884. self.assertEqual(http.NO_CONTENT, response.status_code)
  3885. def test_image_upload_under_quota(self):
  3886. data = b'x' * (self.user_storage_quota - 1)
  3887. self._upload_image_test(data, http.NO_CONTENT)
  3888. def test_image_upload_exceed_quota(self):
  3889. data = b'x' * (self.user_storage_quota + 1)
  3890. self._upload_image_test(data, http.REQUEST_ENTITY_TOO_LARGE)
  3891. def test_chunked_image_upload_under_quota(self):
  3892. def data_gen():
  3893. yield b'x' * (self.user_storage_quota - 1)
  3894. self._upload_image_test(data_gen(), http.NO_CONTENT)
  3895. def test_chunked_image_upload_exceed_quota(self):
  3896. def data_gen():
  3897. yield b'x' * (self.user_storage_quota + 1)
  3898. self._upload_image_test(data_gen(), http.REQUEST_ENTITY_TOO_LARGE)
  3899. class TestQuotasWithRegistry(TestQuotas):
  3900. def setUp(self):
  3901. super(TestQuotasWithRegistry, self).setUp()
  3902. self.api_server.data_api = (
  3903. 'glance.tests.functional.v2.registry_data_api')
  3904. self.registry_server.deployment_flavor = 'trusted-auth'
  3905. class TestImagesMultipleBackend(functional.MultipleBackendFunctionalTest):
  3906. def setUp(self):
  3907. super(TestImagesMultipleBackend, self).setUp()
  3908. self.cleanup()
  3909. self.include_scrubber = False
  3910. self.api_server_multiple_backend.deployment_flavor = 'noauth'
  3911. self.api_server_multiple_backend.data_api = 'glance.db.sqlalchemy.api'
  3912. for i in range(3):
  3913. ret = test_utils.start_http_server("foo_image_id%d" % i,
  3914. "foo_image%d" % i)
  3915. setattr(self, 'http_server%d_pid' % i, ret[0])
  3916. setattr(self, 'http_port%d' % i, ret[1])
  3917. def tearDown(self):
  3918. for i in range(3):
  3919. pid = getattr(self, 'http_server%d_pid' % i, None)
  3920. if pid:
  3921. os.kill(pid, signal.SIGKILL)
  3922. super(TestImagesMultipleBackend, self).tearDown()
  3923. def _url(self, path):
  3924. return 'http://127.0.0.1:%d%s' % (self.api_port, path)
  3925. def _headers(self, custom_headers=None):
  3926. base_headers = {
  3927. 'X-Identity-Status': 'Confirmed',
  3928. 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96',
  3929. 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e',
  3930. 'X-Tenant-Id': TENANT1,
  3931. 'X-Roles': 'member',
  3932. }
  3933. base_headers.update(custom_headers or {})
  3934. return base_headers
  3935. def test_image_import_using_glance_direct(self):
  3936. self.start_servers(**self.__dict__.copy())
  3937. # Image list should be empty
  3938. path = self._url('/v2/images')
  3939. response = requests.get(path, headers=self._headers())
  3940. self.assertEqual(http.OK, response.status_code)
  3941. images = jsonutils.loads(response.text)['images']
  3942. self.assertEqual(0, len(images))
  3943. # glance-direct should be available in discovery response
  3944. path = self._url('/v2/info/import')
  3945. response = requests.get(path, headers=self._headers())
  3946. self.assertEqual(http.OK, response.status_code)
  3947. discovery_calls = jsonutils.loads(
  3948. response.text)['import-methods']['value']
  3949. self.assertIn("glance-direct", discovery_calls)
  3950. # file1 and file2 should be available in discovery response
  3951. available_stores = ['file1', 'file2']
  3952. path = self._url('/v2/info/stores')
  3953. response = requests.get(path, headers=self._headers())
  3954. self.assertEqual(http.OK, response.status_code)
  3955. discovery_calls = jsonutils.loads(
  3956. response.text)['stores']
  3957. for stores in discovery_calls:
  3958. self.assertIn('id', stores)
  3959. self.assertIn(stores['id'], available_stores)
  3960. # Create an image
  3961. path = self._url('/v2/images')
  3962. headers = self._headers({'content-type': 'application/json'})
  3963. data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel',
  3964. 'disk_format': 'aki',
  3965. 'container_format': 'aki'})
  3966. response = requests.post(path, headers=headers, data=data)
  3967. self.assertEqual(http.CREATED, response.status_code)
  3968. # Check 'OpenStack-image-store-ids' header present in response
  3969. self.assertIn('OpenStack-image-store-ids', response.headers)
  3970. for store in available_stores:
  3971. self.assertIn(store, response.headers['OpenStack-image-store-ids'])
  3972. # Returned image entity should have a generated id and status
  3973. image = jsonutils.loads(response.text)
  3974. image_id = image['id']
  3975. checked_keys = set([
  3976. u'status',
  3977. u'name',
  3978. u'tags',
  3979. u'created_at',
  3980. u'updated_at',
  3981. u'visibility',
  3982. u'self',
  3983. u'protected',
  3984. u'id',
  3985. u'file',
  3986. u'min_disk',
  3987. u'type',
  3988. u'min_ram',
  3989. u'schema',
  3990. u'disk_format',
  3991. u'container_format',
  3992. u'owner',
  3993. u'checksum',
  3994. u'size',
  3995. u'virtual_size',
  3996. u'os_hidden',
  3997. u'os_hash_algo',
  3998. u'os_hash_value'
  3999. ])
  4000. self.assertEqual(checked_keys, set(image.keys()))
  4001. expected_image = {
  4002. 'status': 'queued',
  4003. 'name': 'image-1',
  4004. 'tags': [],
  4005. 'visibility': 'shared',
  4006. 'self': '/v2/images/%s' % image_id,
  4007. 'protected': False,
  4008. 'file': '/v2/images/%s/file' % image_id,
  4009. 'min_disk': 0,
  4010. 'type': 'kernel',
  4011. 'min_ram': 0,
  4012. 'schema': '/v2/schemas/image',
  4013. }
  4014. for key, value in expected_image.items():
  4015. self.assertEqual(value, image[key], key)
  4016. # Image list should now have one entry
  4017. path = self._url('/v2/images')
  4018. response = requests.get(path, headers=self._headers())
  4019. self.assertEqual(http.OK, response.status_code)
  4020. images = jsonutils.loads(response.text)['images']
  4021. self.assertEqual(1, len(images))
  4022. self.assertEqual(image_id, images[0]['id'])
  4023. # Upload some image data to staging area
  4024. image_data = b'QQQQQ'
  4025. path = self._url('/v2/images/%s/stage' % image_id)
  4026. headers = self._headers({'Content-Type': 'application/octet-stream'})
  4027. response = requests.put(path, headers=headers, data=image_data)
  4028. self.assertEqual(http.NO_CONTENT, response.status_code)
  4029. # Verify image is in uploading state and checksum is None
  4030. func_utils.verify_image_hashes_and_status(self, image_id,
  4031. status='uploading')
  4032. # Import image to store
  4033. path = self._url('/v2/images/%s/import' % image_id)
  4034. headers = self._headers({
  4035. 'content-type': 'application/json',
  4036. 'X-Roles': 'admin',
  4037. })
  4038. data = jsonutils.dumps({'method': {
  4039. 'name': 'glance-direct'
  4040. }})
  4041. response = requests.post(path, headers=headers, data=data)
  4042. self.assertEqual(http.ACCEPTED, response.status_code)
  4043. # Verify image is in active state and checksum is set
  4044. # NOTE(abhishekk): As import is a async call we need to provide
  4045. # some timelap to complete the call.
  4046. path = self._url('/v2/images/%s' % image_id)
  4047. func_utils.wait_for_status(request_path=path,
  4048. request_headers=self._headers(),
  4049. status='active',
  4050. max_sec=2,
  4051. delay_sec=0.2)
  4052. expect_c = six.text_type(hashlib.md5(image_data).hexdigest())
  4053. expect_h = six.text_type(hashlib.sha512(image_data).hexdigest())
  4054. func_utils.verify_image_hashes_and_status(self,
  4055. image_id,
  4056. checksum=expect_c,
  4057. os_hash_value=expect_h,
  4058. status='active')
  4059. # Ensure the size is updated to reflect the data uploaded
  4060. path = self._url('/v2/images/%s' % image_id)
  4061. response = requests.get(path, headers=self._headers())
  4062. self.assertEqual(http.OK, response.status_code)
  4063. self.assertEqual(len(image_data),
  4064. jsonutils.loads(response.text)['size'])
  4065. # Ensure image is created in default backend
  4066. self.assertIn('file1', jsonutils.loads(response.text)['stores'])
  4067. # Deleting image should work
  4068. path = self._url('/v2/images/%s' % image_id)
  4069. response = requests.delete(path, headers=self._headers())
  4070. self.assertEqual(http.NO_CONTENT, response.status_code)
  4071. # Image list should now be empty
  4072. path = self._url('/v2/images')
  4073. response = requests.get(path, headers=self._headers())
  4074. self.assertEqual(http.OK, response.status_code)
  4075. images = jsonutils.loads(response.text)['images']
  4076. self.assertEqual(0, len(images))
  4077. self.stop_servers()
  4078. def test_image_import_using_glance_direct_different_backend(self):
  4079. self.start_servers(**self.__dict__.copy())
  4080. # Image list should be empty
  4081. path = self._url('/v2/images')
  4082. response = requests.get(path, headers=self._headers())
  4083. self.assertEqual(http.OK, response.status_code)
  4084. images = jsonutils.loads(response.text)['images']
  4085. self.assertEqual(0, len(images))
  4086. # glance-direct should be available in discovery response
  4087. path = self._url('/v2/info/import')
  4088. response = requests.get(path, headers=self._headers())
  4089. self.assertEqual(http.OK, response.status_code)
  4090. discovery_calls = jsonutils.loads(
  4091. response.text)['import-methods']['value']
  4092. self.assertIn("glance-direct", discovery_calls)
  4093. # file1 and file2 should be available in discovery response
  4094. available_stores = ['file1', 'file2']
  4095. path = self._url('/v2/info/stores')
  4096. response = requests.get(path, headers=self._headers())
  4097. self.assertEqual(http.OK, response.status_code)
  4098. discovery_calls = jsonutils.loads(
  4099. response.text)['stores']
  4100. for stores in discovery_calls:
  4101. self.assertIn('id', stores)
  4102. self.assertIn(stores['id'], available_stores)
  4103. # Create an image
  4104. path = self._url('/v2/images')
  4105. headers = self._headers({'content-type': 'application/json'})
  4106. data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel',
  4107. 'disk_format': 'aki',
  4108. 'container_format': 'aki'})
  4109. response = requests.post(path, headers=headers, data=data)
  4110. self.assertEqual(http.CREATED, response.status_code)
  4111. # Check 'OpenStack-image-store-ids' header present in response
  4112. self.assertIn('OpenStack-image-store-ids', response.headers)
  4113. for store in available_stores:
  4114. self.assertIn(store, response.headers['OpenStack-image-store-ids'])
  4115. # Returned image entity should have a generated id and status
  4116. image = jsonutils.loads(response.text)
  4117. image_id = image['id']
  4118. checked_keys = set([
  4119. u'status',
  4120. u'name',
  4121. u'tags',
  4122. u'created_at',
  4123. u'updated_at',
  4124. u'visibility',
  4125. u'self',
  4126. u'protected',
  4127. u'id',
  4128. u'file',
  4129. u'min_disk',
  4130. u'type',
  4131. u'min_ram',
  4132. u'schema',
  4133. u'disk_format',
  4134. u'container_format',
  4135. u'owner',
  4136. u'checksum',
  4137. u'size',
  4138. u'virtual_size',
  4139. u'os_hidden',
  4140. u'os_hash_algo',
  4141. u'os_hash_value'
  4142. ])
  4143. self.assertEqual(checked_keys, set(image.keys()))
  4144. expected_image = {
  4145. 'status': 'queued',
  4146. 'name': 'image-1',
  4147. 'tags': [],
  4148. 'visibility': 'shared',
  4149. 'self': '/v2/images/%s' % image_id,
  4150. 'protected': False,
  4151. 'file': '/v2/images/%s/file' % image_id,
  4152. 'min_disk': 0,
  4153. 'type': 'kernel',
  4154. 'min_ram': 0,
  4155. 'schema': '/v2/schemas/image',
  4156. }
  4157. for key, value in expected_image.items():
  4158. self.assertEqual(value, image[key], key)
  4159. # Image list should now have one entry
  4160. path = self._url('/v2/images')
  4161. response = requests.get(path, headers=self._headers())
  4162. self.assertEqual(http.OK, response.status_code)
  4163. images = jsonutils.loads(response.text)['images']
  4164. self.assertEqual(1, len(images))
  4165. self.assertEqual(image_id, images[0]['id'])
  4166. # Upload some image data to staging area
  4167. image_data = b'GLANCE IS DEAD SEXY'
  4168. path = self._url('/v2/images/%s/stage' % image_id)
  4169. headers = self._headers({'Content-Type': 'application/octet-stream'})
  4170. response = requests.put(path, headers=headers, data=image_data)
  4171. self.assertEqual(http.NO_CONTENT, response.status_code)
  4172. # Verify image is in uploading state and checksum is None
  4173. func_utils.verify_image_hashes_and_status(self, image_id,
  4174. status='uploading')
  4175. # Import image to file2 store (other than default backend)
  4176. path = self._url('/v2/images/%s/import' % image_id)
  4177. headers = self._headers({
  4178. 'content-type': 'application/json',
  4179. 'X-Roles': 'admin',
  4180. 'X-Image-Meta-Store': 'file2'
  4181. })
  4182. data = jsonutils.dumps({'method': {
  4183. 'name': 'glance-direct'
  4184. }})
  4185. response = requests.post(path, headers=headers, data=data)
  4186. self.assertEqual(http.ACCEPTED, response.status_code)
  4187. # Verify image is in active state and checksum is set
  4188. # NOTE(abhishekk): As import is a async call we need to provide
  4189. # some timelap to complete the call.
  4190. path = self._url('/v2/images/%s' % image_id)
  4191. func_utils.wait_for_status(request_path=path,
  4192. request_headers=self._headers(),
  4193. status='active',
  4194. max_sec=2,
  4195. delay_sec=0.2)
  4196. expect_c = six.text_type(hashlib.md5(image_data).hexdigest())
  4197. expect_h = six.text_type(hashlib.sha512(image_data).hexdigest())
  4198. func_utils.verify_image_hashes_and_status(self,
  4199. image_id,
  4200. checksum=expect_c,
  4201. os_hash_value=expect_h,
  4202. status='active')
  4203. # Ensure the size is updated to reflect the data uploaded
  4204. path = self._url('/v2/images/%s' % image_id)
  4205. response = requests.get(path, headers=self._headers())
  4206. self.assertEqual(http.OK, response.status_code)
  4207. self.assertEqual(len(image_data),
  4208. jsonutils.loads(response.text)['size'])
  4209. # Ensure image is created in different backend
  4210. self.assertIn('file2', jsonutils.loads(response.text)['stores'])
  4211. # Deleting image should work
  4212. path = self._url('/v2/images/%s' % image_id)
  4213. response = requests.delete(path, headers=self._headers())
  4214. self.assertEqual(http.NO_CONTENT, response.status_code)
  4215. # Image list should now be empty
  4216. path = self._url('/v2/images')
  4217. response = requests.get(path, headers=self._headers())
  4218. self.assertEqual(http.OK, response.status_code)
  4219. images = jsonutils.loads(response.text)['images']
  4220. self.assertEqual(0, len(images))
  4221. self.stop_servers()
  4222. def test_image_import_using_web_download(self):
  4223. self.config(node_staging_uri="file:///tmp/staging/")
  4224. self.start_servers(**self.__dict__.copy())
  4225. # Image list should be empty
  4226. path = self._url('/v2/images')
  4227. response = requests.get(path, headers=self._headers())
  4228. self.assertEqual(http.OK, response.status_code)
  4229. images = jsonutils.loads(response.text)['images']
  4230. self.assertEqual(0, len(images))
  4231. # web-download should be available in discovery response
  4232. path = self._url('/v2/info/import')
  4233. response = requests.get(path, headers=self._headers())
  4234. self.assertEqual(http.OK, response.status_code)
  4235. discovery_calls = jsonutils.loads(
  4236. response.text)['import-methods']['value']
  4237. self.assertIn("web-download", discovery_calls)
  4238. # file1 and file2 should be available in discovery response
  4239. available_stores = ['file1', 'file2']
  4240. path = self._url('/v2/info/stores')
  4241. response = requests.get(path, headers=self._headers())
  4242. self.assertEqual(http.OK, response.status_code)
  4243. discovery_calls = jsonutils.loads(
  4244. response.text)['stores']
  4245. for stores in discovery_calls:
  4246. self.assertIn('id', stores)
  4247. self.assertIn(stores['id'], available_stores)
  4248. # Create an image
  4249. path = self._url('/v2/images')
  4250. headers = self._headers({'content-type': 'application/json'})
  4251. data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel',
  4252. 'disk_format': 'aki',
  4253. 'container_format': 'aki'})
  4254. response = requests.post(path, headers=headers, data=data)
  4255. self.assertEqual(http.CREATED, response.status_code)
  4256. # Check 'OpenStack-image-store-ids' header present in response
  4257. self.assertIn('OpenStack-image-store-ids', response.headers)
  4258. for store in available_stores:
  4259. self.assertIn(store, response.headers['OpenStack-image-store-ids'])
  4260. # Returned image entity should have a generated id and status
  4261. image = jsonutils.loads(response.text)
  4262. image_id = image['id']
  4263. checked_keys = set([
  4264. u'status',
  4265. u'name',
  4266. u'tags',
  4267. u'created_at',
  4268. u'updated_at',
  4269. u'visibility',
  4270. u'self',
  4271. u'protected',
  4272. u'id',
  4273. u'file',
  4274. u'min_disk',
  4275. u'type',
  4276. u'min_ram',
  4277. u'schema',
  4278. u'disk_format',
  4279. u'container_format',
  4280. u'owner',
  4281. u'checksum',
  4282. u'size',
  4283. u'virtual_size',
  4284. u'os_hidden',
  4285. u'os_hash_algo',
  4286. u'os_hash_value'
  4287. ])
  4288. self.assertEqual(checked_keys, set(image.keys()))
  4289. expected_image = {
  4290. 'status': 'queued',
  4291. 'name': 'image-1',
  4292. 'tags': [],
  4293. 'visibility': 'shared',
  4294. 'self': '/v2/images/%s' % image_id,
  4295. 'protected': False,
  4296. 'file': '/v2/images/%s/file' % image_id,
  4297. 'min_disk': 0,
  4298. 'type': 'kernel',
  4299. 'min_ram': 0,
  4300. 'schema': '/v2/schemas/image',
  4301. }
  4302. for key, value in expected_image.items():
  4303. self.assertEqual(value, image[key], key)
  4304. # Image list should now have one entry
  4305. path = self._url('/v2/images')
  4306. response = requests.get(path, headers=self._headers())
  4307. self.assertEqual(http.OK, response.status_code)
  4308. images = jsonutils.loads(response.text)['images']
  4309. self.assertEqual(1, len(images))
  4310. self.assertEqual(image_id, images[0]['id'])
  4311. # Verify image is in queued state and checksum is None
  4312. func_utils.verify_image_hashes_and_status(self, image_id,
  4313. status='queued')
  4314. # Import image to store
  4315. path = self._url('/v2/images/%s/import' % image_id)
  4316. headers = self._headers({
  4317. 'content-type': 'application/json',
  4318. 'X-Roles': 'admin',
  4319. })
  4320. image_data_uri = ('https://www.openstack.org/assets/openstack-logo/'
  4321. '2016R/OpenStack-Logo-Horizontal.eps.zip')
  4322. data = jsonutils.dumps({'method': {
  4323. 'name': 'web-download',
  4324. 'uri': image_data_uri
  4325. }})
  4326. response = requests.post(path, headers=headers, data=data)
  4327. self.assertEqual(http.ACCEPTED, response.status_code)
  4328. # Verify image is in active state and checksum is set
  4329. # NOTE(abhishekk): As import is a async call we need to provide
  4330. # some timelap to complete the call.
  4331. path = self._url('/v2/images/%s' % image_id)
  4332. func_utils.wait_for_status(request_path=path,
  4333. request_headers=self._headers(),
  4334. status='active',
  4335. max_sec=20,
  4336. delay_sec=0.2,
  4337. start_delay_sec=1)
  4338. with requests.get(image_data_uri) as r:
  4339. expect_c = six.text_type(hashlib.md5(r.content).hexdigest())
  4340. expect_h = six.text_type(hashlib.sha512(r.content).hexdigest())
  4341. func_utils.verify_image_hashes_and_status(self,
  4342. image_id,
  4343. checksum=expect_c,
  4344. os_hash_value=expect_h,
  4345. status='active')
  4346. # Ensure image is created in default backend
  4347. path = self._url('/v2/images/%s' % image_id)
  4348. response = requests.get(path, headers=self._headers())
  4349. self.assertEqual(http.OK, response.status_code)
  4350. self.assertIn('file1', jsonutils.loads(response.text)['stores'])
  4351. # Deleting image should work
  4352. path = self._url('/v2/images/%s' % image_id)
  4353. response = requests.delete(path, headers=self._headers())
  4354. self.assertEqual(http.NO_CONTENT, response.status_code)
  4355. # Image list should now be empty
  4356. path = self._url('/v2/images')
  4357. response = requests.get(path, headers=self._headers())
  4358. self.assertEqual(http.OK, response.status_code)
  4359. images = jsonutils.loads(response.text)['images']
  4360. self.assertEqual(0, len(images))
  4361. self.stop_servers()
  4362. def test_image_import_using_web_download_different_backend(self):
  4363. self.config(node_staging_uri="file:///tmp/staging/")
  4364. self.start_servers(**self.__dict__.copy())
  4365. # Image list should be empty
  4366. path = self._url('/v2/images')
  4367. response = requests.get(path, headers=self._headers())
  4368. self.assertEqual(http.OK, response.status_code)
  4369. images = jsonutils.loads(response.text)['images']
  4370. self.assertEqual(0, len(images))
  4371. # web-download should be available in discovery response
  4372. path = self._url('/v2/info/import')
  4373. response = requests.get(path, headers=self._headers())
  4374. self.assertEqual(http.OK, response.status_code)
  4375. discovery_calls = jsonutils.loads(
  4376. response.text)['import-methods']['value']
  4377. self.assertIn("web-download", discovery_calls)
  4378. # file1 and file2 should be available in discovery response
  4379. available_stores = ['file1', 'file2']
  4380. path = self._url('/v2/info/stores')
  4381. response = requests.get(path, headers=self._headers())
  4382. self.assertEqual(http.OK, response.status_code)
  4383. discovery_calls = jsonutils.loads(
  4384. response.text)['stores']
  4385. for stores in discovery_calls:
  4386. self.assertIn('id', stores)
  4387. self.assertIn(stores['id'], available_stores)
  4388. # Create an image
  4389. path = self._url('/v2/images')
  4390. headers = self._headers({'content-type': 'application/json'})
  4391. data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel',
  4392. 'disk_format': 'aki',
  4393. 'container_format': 'aki'})
  4394. response = requests.post(path, headers=headers, data=data)
  4395. self.assertEqual(http.CREATED, response.status_code)
  4396. # Check 'OpenStack-image-store-ids' header present in response
  4397. self.assertIn('OpenStack-image-store-ids', response.headers)
  4398. for store in available_stores:
  4399. self.assertIn(store, response.headers['OpenStack-image-store-ids'])
  4400. # Returned image entity should have a generated id and status
  4401. image = jsonutils.loads(response.text)
  4402. image_id = image['id']
  4403. checked_keys = set([
  4404. u'status',
  4405. u'name',
  4406. u'tags',
  4407. u'created_at',
  4408. u'updated_at',
  4409. u'visibility',
  4410. u'self',
  4411. u'protected',
  4412. u'id',
  4413. u'file',
  4414. u'min_disk',
  4415. u'type',
  4416. u'min_ram',
  4417. u'schema',
  4418. u'disk_format',
  4419. u'container_format',
  4420. u'owner',
  4421. u'checksum',
  4422. u'size',
  4423. u'virtual_size',
  4424. u'os_hidden',
  4425. u'os_hash_algo',
  4426. u'os_hash_value'
  4427. ])
  4428. self.assertEqual(checked_keys, set(image.keys()))
  4429. expected_image = {
  4430. 'status': 'queued',
  4431. 'name': 'image-1',
  4432. 'tags': [],
  4433. 'visibility': 'shared',
  4434. 'self': '/v2/images/%s' % image_id,
  4435. 'protected': False,
  4436. 'file': '/v2/images/%s/file' % image_id,
  4437. 'min_disk': 0,
  4438. 'type': 'kernel',
  4439. 'min_ram': 0,
  4440. 'schema': '/v2/schemas/image',
  4441. }
  4442. for key, value in expected_image.items():
  4443. self.assertEqual(value, image[key], key)
  4444. # Image list should now have one entry
  4445. path = self._url('/v2/images')
  4446. response = requests.get(path, headers=self._headers())
  4447. self.assertEqual(http.OK, response.status_code)
  4448. images = jsonutils.loads(response.text)['images']
  4449. self.assertEqual(1, len(images))
  4450. self.assertEqual(image_id, images[0]['id'])
  4451. # Verify image is in queued state and checksum is None
  4452. func_utils.verify_image_hashes_and_status(self, image_id,
  4453. status='queued')
  4454. # Import image to store
  4455. path = self._url('/v2/images/%s/import' % image_id)
  4456. headers = self._headers({
  4457. 'content-type': 'application/json',
  4458. 'X-Roles': 'admin',
  4459. 'X-Image-Meta-Store': 'file2'
  4460. })
  4461. image_data_uri = ('https://www.openstack.org/assets/openstack-logo/'
  4462. '2016R/OpenStack-Logo-Horizontal.eps.zip')
  4463. data = jsonutils.dumps({'method': {
  4464. 'name': 'web-download',
  4465. 'uri': image_data_uri
  4466. }})
  4467. response = requests.post(path, headers=headers, data=data)
  4468. self.assertEqual(http.ACCEPTED, response.status_code)
  4469. # Verify image is in active state and checksum is set
  4470. # NOTE(abhishekk): As import is a async call we need to provide
  4471. # some timelap to complete the call.
  4472. path = self._url('/v2/images/%s' % image_id)
  4473. func_utils.wait_for_status(request_path=path,
  4474. request_headers=self._headers(),
  4475. status='active',
  4476. max_sec=20,
  4477. delay_sec=0.2,
  4478. start_delay_sec=1)
  4479. with requests.get(image_data_uri) as r:
  4480. expect_c = six.text_type(hashlib.md5(r.content).hexdigest())
  4481. expect_h = six.text_type(hashlib.sha512(r.content).hexdigest())
  4482. func_utils.verify_image_hashes_and_status(self,
  4483. image_id,
  4484. checksum=expect_c,
  4485. os_hash_value=expect_h,
  4486. status='active')
  4487. # Ensure image is created in different backend
  4488. path = self._url('/v2/images/%s' % image_id)
  4489. response = requests.get(path, headers=self._headers())
  4490. self.assertEqual(http.OK, response.status_code)
  4491. self.assertIn('file2', jsonutils.loads(response.text)['stores'])
  4492. # Deleting image should work
  4493. path = self._url('/v2/images/%s' % image_id)
  4494. response = requests.delete(path, headers=self._headers())
  4495. self.assertEqual(http.NO_CONTENT, response.status_code)
  4496. # Image list should now be empty
  4497. path = self._url('/v2/images')
  4498. response = requests.get(path, headers=self._headers())
  4499. self.assertEqual(http.OK, response.status_code)
  4500. images = jsonutils.loads(response.text)['images']
  4501. self.assertEqual(0, len(images))
  4502. self.stop_servers()
  4503. def test_image_lifecycle(self):
  4504. # Image list should be empty
  4505. self.start_servers(**self.__dict__.copy())
  4506. path = self._url('/v2/images')
  4507. response = requests.get(path, headers=self._headers())
  4508. self.assertEqual(http.OK, response.status_code)
  4509. images = jsonutils.loads(response.text)['images']
  4510. self.assertEqual(0, len(images))
  4511. # file1 and file2 should be available in discovery response
  4512. available_stores = ['file1', 'file2']
  4513. path = self._url('/v2/info/stores')
  4514. response = requests.get(path, headers=self._headers())
  4515. self.assertEqual(http.OK, response.status_code)
  4516. discovery_calls = jsonutils.loads(
  4517. response.text)['stores']
  4518. for stores in discovery_calls:
  4519. self.assertIn('id', stores)
  4520. self.assertIn(stores['id'], available_stores)
  4521. # Create an image (with two deployer-defined properties)
  4522. path = self._url('/v2/images')
  4523. headers = self._headers({'content-type': 'application/json'})
  4524. data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel',
  4525. 'foo': 'bar', 'disk_format': 'aki',
  4526. 'container_format': 'aki', 'abc': 'xyz',
  4527. 'protected': True})
  4528. response = requests.post(path, headers=headers, data=data)
  4529. self.assertEqual(http.CREATED, response.status_code)
  4530. # Check 'OpenStack-image-store-ids' header present in response
  4531. self.assertIn('OpenStack-image-store-ids', response.headers)
  4532. for store in available_stores:
  4533. self.assertIn(store, response.headers['OpenStack-image-store-ids'])
  4534. # Returned image entity should have a generated id and status
  4535. image = jsonutils.loads(response.text)
  4536. image_id = image['id']
  4537. checked_keys = set([
  4538. u'status',
  4539. u'name',
  4540. u'tags',
  4541. u'created_at',
  4542. u'updated_at',
  4543. u'visibility',
  4544. u'self',
  4545. u'protected',
  4546. u'id',
  4547. u'file',
  4548. u'min_disk',
  4549. u'foo',
  4550. u'abc',
  4551. u'type',
  4552. u'min_ram',
  4553. u'schema',
  4554. u'disk_format',
  4555. u'container_format',
  4556. u'owner',
  4557. u'checksum',
  4558. u'size',
  4559. u'virtual_size',
  4560. u'os_hidden',
  4561. u'os_hash_algo',
  4562. u'os_hash_value'
  4563. ])
  4564. self.assertEqual(checked_keys, set(image.keys()))
  4565. expected_image = {
  4566. 'status': 'queued',
  4567. 'name': 'image-1',
  4568. 'tags': [],
  4569. 'visibility': 'shared',
  4570. 'self': '/v2/images/%s' % image_id,
  4571. 'protected': True,
  4572. 'file': '/v2/images/%s/file' % image_id,
  4573. 'min_disk': 0,
  4574. 'foo': 'bar',
  4575. 'abc': 'xyz',
  4576. 'type': 'kernel',
  4577. 'min_ram': 0,
  4578. 'schema': '/v2/schemas/image',
  4579. }
  4580. for key, value in expected_image.items():
  4581. self.assertEqual(value, image[key], key)
  4582. # Image list should now have one entry
  4583. path = self._url('/v2/images')
  4584. response = requests.get(path, headers=self._headers())
  4585. self.assertEqual(http.OK, response.status_code)
  4586. images = jsonutils.loads(response.text)['images']
  4587. self.assertEqual(1, len(images))
  4588. self.assertEqual(image_id, images[0]['id'])
  4589. # Try to download data before its uploaded
  4590. path = self._url('/v2/images/%s/file' % image_id)
  4591. headers = self._headers()
  4592. response = requests.get(path, headers=headers)
  4593. self.assertEqual(http.NO_CONTENT, response.status_code)
  4594. # Upload some image data
  4595. image_data = b'OpenStack Rules, Other Clouds Drool'
  4596. path = self._url('/v2/images/%s/file' % image_id)
  4597. headers = self._headers({'Content-Type': 'application/octet-stream'})
  4598. response = requests.put(path, headers=headers, data=image_data)
  4599. self.assertEqual(http.NO_CONTENT, response.status_code)
  4600. expect_c = six.text_type(hashlib.md5(image_data).hexdigest())
  4601. expect_h = six.text_type(hashlib.sha512(image_data).hexdigest())
  4602. func_utils.verify_image_hashes_and_status(self,
  4603. image_id,
  4604. checksum=expect_c,
  4605. os_hash_value=expect_h,
  4606. status='active')
  4607. # Ensure image is created in default backend
  4608. path = self._url('/v2/images/%s' % image_id)
  4609. response = requests.get(path, headers=self._headers())
  4610. self.assertEqual(http.OK, response.status_code)
  4611. self.assertIn('file1', jsonutils.loads(response.text)['stores'])
  4612. # Try to download the data that was just uploaded
  4613. path = self._url('/v2/images/%s/file' % image_id)
  4614. response = requests.get(path, headers=self._headers())
  4615. self.assertEqual(http.OK, response.status_code)
  4616. self.assertEqual(expect_c, response.headers['Content-MD5'])
  4617. self.assertEqual(image_data.decode('utf-8'), response.text)
  4618. # Ensure the size is updated to reflect the data uploaded
  4619. path = self._url('/v2/images/%s' % image_id)
  4620. response = requests.get(path, headers=self._headers())
  4621. self.assertEqual(http.OK, response.status_code)
  4622. self.assertEqual(len(image_data),
  4623. jsonutils.loads(response.text)['size'])
  4624. # Unprotect image for deletion
  4625. path = self._url('/v2/images/%s' % image_id)
  4626. media_type = 'application/openstack-images-v2.1-json-patch'
  4627. headers = self._headers({'content-type': media_type})
  4628. doc = [{'op': 'replace', 'path': '/protected', 'value': False}]
  4629. data = jsonutils.dumps(doc)
  4630. response = requests.patch(path, headers=headers, data=data)
  4631. self.assertEqual(http.OK, response.status_code, response.text)
  4632. # Deletion should work. Deleting image
  4633. path = self._url('/v2/images/%s' % image_id)
  4634. response = requests.delete(path, headers=self._headers())
  4635. self.assertEqual(http.NO_CONTENT, response.status_code)
  4636. # This image should be no longer be directly accessible
  4637. path = self._url('/v2/images/%s' % image_id)
  4638. response = requests.get(path, headers=self._headers())
  4639. self.assertEqual(http.NOT_FOUND, response.status_code)
  4640. # And neither should its data
  4641. path = self._url('/v2/images/%s/file' % image_id)
  4642. headers = self._headers()
  4643. response = requests.get(path, headers=headers)
  4644. self.assertEqual(http.NOT_FOUND, response.status_code)
  4645. # Image list should now be empty
  4646. path = self._url('/v2/images')
  4647. response = requests.get(path, headers=self._headers())
  4648. self.assertEqual(http.OK, response.status_code)
  4649. images = jsonutils.loads(response.text)['images']
  4650. self.assertEqual(0, len(images))
  4651. self.stop_servers()
  4652. def test_image_lifecycle_different_backend(self):
  4653. # Image list should be empty
  4654. self.start_servers(**self.__dict__.copy())
  4655. path = self._url('/v2/images')
  4656. response = requests.get(path, headers=self._headers())
  4657. self.assertEqual(http.OK, response.status_code)
  4658. images = jsonutils.loads(response.text)['images']
  4659. self.assertEqual(0, len(images))
  4660. # file1 and file2 should be available in discovery response
  4661. available_stores = ['file1', 'file2']
  4662. path = self._url('/v2/info/stores')
  4663. response = requests.get(path, headers=self._headers())
  4664. self.assertEqual(http.OK, response.status_code)
  4665. discovery_calls = jsonutils.loads(
  4666. response.text)['stores']
  4667. for stores in discovery_calls:
  4668. self.assertIn('id', stores)
  4669. self.assertIn(stores['id'], available_stores)
  4670. # Create an image (with two deployer-defined properties)
  4671. path = self._url('/v2/images')
  4672. headers = self._headers({'content-type': 'application/json'})
  4673. data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel',
  4674. 'foo': 'bar', 'disk_format': 'aki',
  4675. 'container_format': 'aki', 'abc': 'xyz',
  4676. 'protected': True})
  4677. response = requests.post(path, headers=headers, data=data)
  4678. self.assertEqual(http.CREATED, response.status_code)
  4679. # Check 'OpenStack-image-store-ids' header present in response
  4680. self.assertIn('OpenStack-image-store-ids', response.headers)
  4681. for store in available_stores:
  4682. self.assertIn(store, response.headers['OpenStack-image-store-ids'])
  4683. # Returned image entity should have a generated id and status
  4684. image = jsonutils.loads(response.text)
  4685. image_id = image['id']
  4686. checked_keys = set([
  4687. u'status',
  4688. u'name',
  4689. u'tags',
  4690. u'created_at',
  4691. u'updated_at',
  4692. u'visibility',
  4693. u'self',
  4694. u'protected',
  4695. u'id',
  4696. u'file',
  4697. u'min_disk',
  4698. u'foo',
  4699. u'abc',
  4700. u'type',
  4701. u'min_ram',
  4702. u'schema',
  4703. u'disk_format',
  4704. u'container_format',
  4705. u'owner',
  4706. u'checksum',
  4707. u'size',
  4708. u'virtual_size',
  4709. u'os_hidden',
  4710. u'os_hash_algo',
  4711. u'os_hash_value'
  4712. ])
  4713. self.assertEqual(checked_keys, set(image.keys()))
  4714. expected_image = {
  4715. 'status': 'queued',
  4716. 'name': 'image-1',
  4717. 'tags': [],
  4718. 'visibility': 'shared',
  4719. 'self': '/v2/images/%s' % image_id,
  4720. 'protected': True,
  4721. 'file': '/v2/images/%s/file' % image_id,
  4722. 'min_disk': 0,
  4723. 'foo': 'bar',
  4724. 'abc': 'xyz',
  4725. 'type': 'kernel',
  4726. 'min_ram': 0,
  4727. 'schema': '/v2/schemas/image',
  4728. }
  4729. for key, value in expected_image.items():
  4730. self.assertEqual(value, image[key], key)
  4731. # Image list should now have one entry
  4732. path = self._url('/v2/images')
  4733. response = requests.get(path, headers=self._headers())
  4734. self.assertEqual(http.OK, response.status_code)
  4735. images = jsonutils.loads(response.text)['images']
  4736. self.assertEqual(1, len(images))
  4737. self.assertEqual(image_id, images[0]['id'])
  4738. # Try to download data before its uploaded
  4739. path = self._url('/v2/images/%s/file' % image_id)
  4740. headers = self._headers()
  4741. response = requests.get(path, headers=headers)
  4742. self.assertEqual(http.NO_CONTENT, response.status_code)
  4743. # Upload some image data
  4744. image_data = b'just a passing glance'
  4745. path = self._url('/v2/images/%s/file' % image_id)
  4746. headers = self._headers({
  4747. 'Content-Type': 'application/octet-stream',
  4748. 'X-Image-Meta-Store': 'file2'
  4749. })
  4750. response = requests.put(path, headers=headers, data=image_data)
  4751. self.assertEqual(http.NO_CONTENT, response.status_code)
  4752. expect_c = six.text_type(hashlib.md5(image_data).hexdigest())
  4753. expect_h = six.text_type(hashlib.sha512(image_data).hexdigest())
  4754. func_utils.verify_image_hashes_and_status(self,
  4755. image_id,
  4756. checksum=expect_c,
  4757. os_hash_value=expect_h,
  4758. status='active')
  4759. # Ensure image is created in different backend
  4760. path = self._url('/v2/images/%s' % image_id)
  4761. response = requests.get(path, headers=self._headers())
  4762. self.assertEqual(http.OK, response.status_code)
  4763. self.assertIn('file2', jsonutils.loads(response.text)['stores'])
  4764. # Try to download the data that was just uploaded
  4765. path = self._url('/v2/images/%s/file' % image_id)
  4766. response = requests.get(path, headers=self._headers())
  4767. self.assertEqual(http.OK, response.status_code)
  4768. self.assertEqual(expect_c, response.headers['Content-MD5'])
  4769. self.assertEqual(image_data.decode('utf-8'), response.text)
  4770. # Ensure the size is updated to reflect the data uploaded
  4771. path = self._url('/v2/images/%s' % image_id)
  4772. response = requests.get(path, headers=self._headers())
  4773. self.assertEqual(http.OK, response.status_code)
  4774. self.assertEqual(len(image_data),
  4775. jsonutils.loads(response.text)['size'])
  4776. # Unprotect image for deletion
  4777. path = self._url('/v2/images/%s' % image_id)
  4778. media_type = 'application/openstack-images-v2.1-json-patch'
  4779. headers = self._headers({'content-type': media_type})
  4780. doc = [{'op': 'replace', 'path': '/protected', 'value': False}]
  4781. data = jsonutils.dumps(doc)
  4782. response = requests.patch(path, headers=headers, data=data)
  4783. self.assertEqual(http.OK, response.status_code, response.text)
  4784. # Deletion should work. Deleting image
  4785. path = self._url('/v2/images/%s' % image_id)
  4786. response = requests.delete(path, headers=self._headers())
  4787. self.assertEqual(http.NO_CONTENT, response.status_code)
  4788. # This image should be no longer be directly accessible
  4789. path = self._url('/v2/images/%s' % image_id)
  4790. response = requests.get(path, headers=self._headers())
  4791. self.assertEqual(http.NOT_FOUND, response.status_code)
  4792. # And neither should its data
  4793. path = self._url('/v2/images/%s/file' % image_id)
  4794. headers = self._headers()
  4795. response = requests.get(path, headers=headers)
  4796. self.assertEqual(http.NOT_FOUND, response.status_code)
  4797. # Image list should now be empty
  4798. path = self._url('/v2/images')
  4799. response = requests.get(path, headers=self._headers())
  4800. self.assertEqual(http.OK, response.status_code)
  4801. images = jsonutils.loads(response.text)['images']
  4802. self.assertEqual(0, len(images))
  4803. self.stop_servers()