Browse Source

Unit/Functional tests for multi store support

Added some unit tests for coverage purpose.
Added functional tests for create and import scenarios.

Note:
For functional tests I have considered file store with two
different image directories.

Related to blueprint multi-store
Change-Id: I59e28ab822fb5f6940f48ddbf6dfba4cb7d4c509
tags/17.0.0.0rc1
Abhishek Kekane 11 months ago
parent
commit
73109de485

+ 556
- 0
glance/tests/functional/__init__.py View File

@@ -445,6 +445,191 @@ allowed_origin=http://valid.example.com
445 445
 """
446 446
 
447 447
 
448
+class ApiServerForMultipleBackend(Server):
449
+
450
+    """
451
+    Server object that starts/stops/manages the API server
452
+    """
453
+
454
+    def __init__(self, test_dir, port, policy_file, delayed_delete=False,
455
+                 pid_file=None, sock=None, **kwargs):
456
+        super(ApiServerForMultipleBackend, self).__init__(
457
+            test_dir, port, sock=sock)
458
+        self.server_name = 'api'
459
+        self.server_module = 'glance.cmd.%s' % self.server_name
460
+        self.default_backend = kwargs.get("default_backend", "file1")
461
+        self.bind_host = "127.0.0.1"
462
+        self.registry_host = "127.0.0.1"
463
+        self.key_file = ""
464
+        self.cert_file = ""
465
+        self.metadata_encryption_key = "012345678901234567890123456789ab"
466
+        self.image_dir_backend_1 = os.path.join(self.test_dir, "images_1")
467
+        self.image_dir_backend_2 = os.path.join(self.test_dir, "images_2")
468
+        self.pid_file = pid_file or os.path.join(self.test_dir,
469
+                                                 "multiple_backend_api.pid")
470
+        self.log_file = os.path.join(self.test_dir, "multiple_backend_api.log")
471
+        self.image_size_cap = 1099511627776
472
+        self.delayed_delete = delayed_delete
473
+        self.owner_is_tenant = True
474
+        self.workers = 0
475
+        self.scrub_time = 5
476
+        self.image_cache_dir = os.path.join(self.test_dir,
477
+                                            'cache')
478
+        self.image_cache_driver = 'sqlite'
479
+        self.policy_file = policy_file
480
+        self.policy_default_rule = 'default'
481
+        self.property_protection_rule_format = 'roles'
482
+        self.image_member_quota = 10
483
+        self.image_property_quota = 10
484
+        self.image_tag_quota = 10
485
+        self.image_location_quota = 2
486
+        self.disable_path = None
487
+
488
+        self.needs_database = True
489
+        default_sql_connection = 'sqlite:////%s/tests.sqlite' % self.test_dir
490
+        self.sql_connection = os.environ.get('GLANCE_TEST_SQL_CONNECTION',
491
+                                             default_sql_connection)
492
+        self.data_api = kwargs.get("data_api",
493
+                                   "glance.db.sqlalchemy.api")
494
+        self.user_storage_quota = '0'
495
+        self.lock_path = self.test_dir
496
+
497
+        self.location_strategy = 'location_order'
498
+        self.store_type_location_strategy_preference = ""
499
+
500
+        self.send_identity_headers = False
501
+
502
+        self.conf_base = """[DEFAULT]
503
+debug = %(debug)s
504
+default_log_levels = eventlet.wsgi.server=DEBUG
505
+bind_host = %(bind_host)s
506
+bind_port = %(bind_port)s
507
+key_file = %(key_file)s
508
+cert_file = %(cert_file)s
509
+metadata_encryption_key = %(metadata_encryption_key)s
510
+registry_host = %(registry_host)s
511
+registry_port = %(registry_port)s
512
+use_user_token = %(use_user_token)s
513
+send_identity_credentials = %(send_identity_credentials)s
514
+log_file = %(log_file)s
515
+image_size_cap = %(image_size_cap)d
516
+delayed_delete = %(delayed_delete)s
517
+owner_is_tenant = %(owner_is_tenant)s
518
+workers = %(workers)s
519
+scrub_time = %(scrub_time)s
520
+send_identity_headers = %(send_identity_headers)s
521
+image_cache_dir = %(image_cache_dir)s
522
+image_cache_driver = %(image_cache_driver)s
523
+data_api = %(data_api)s
524
+sql_connection = %(sql_connection)s
525
+show_image_direct_url = %(show_image_direct_url)s
526
+show_multiple_locations = %(show_multiple_locations)s
527
+user_storage_quota = %(user_storage_quota)s
528
+enable_v2_api = %(enable_v2_api)s
529
+lock_path = %(lock_path)s
530
+property_protection_file = %(property_protection_file)s
531
+property_protection_rule_format = %(property_protection_rule_format)s
532
+image_member_quota=%(image_member_quota)s
533
+image_property_quota=%(image_property_quota)s
534
+image_tag_quota=%(image_tag_quota)s
535
+image_location_quota=%(image_location_quota)s
536
+location_strategy=%(location_strategy)s
537
+allow_additional_image_properties = True
538
+enabled_backends=file1:file, file2:file
539
+[oslo_policy]
540
+policy_file = %(policy_file)s
541
+policy_default_rule = %(policy_default_rule)s
542
+[paste_deploy]
543
+flavor = %(deployment_flavor)s
544
+[store_type_location_strategy]
545
+store_type_preference = %(store_type_location_strategy_preference)s
546
+[glance_store]
547
+default_backend = %(default_backend)s
548
+[file1]
549
+filesystem_store_datadir=%(image_dir_backend_1)s
550
+[file2]
551
+filesystem_store_datadir=%(image_dir_backend_2)s
552
+"""
553
+        self.paste_conf_base = """[pipeline:glance-api]
554
+pipeline =
555
+    cors
556
+    healthcheck
557
+    versionnegotiation
558
+    gzip
559
+    unauthenticated-context
560
+    rootapp
561
+
562
+[pipeline:glance-api-caching]
563
+pipeline = cors healthcheck versionnegotiation gzip unauthenticated-context
564
+ cache rootapp
565
+
566
+[pipeline:glance-api-cachemanagement]
567
+pipeline =
568
+    cors
569
+    healthcheck
570
+    versionnegotiation
571
+    gzip
572
+    unauthenticated-context
573
+    cache
574
+    cache_manage
575
+    rootapp
576
+
577
+[pipeline:glance-api-fakeauth]
578
+pipeline = cors healthcheck versionnegotiation gzip fakeauth context rootapp
579
+
580
+[pipeline:glance-api-noauth]
581
+pipeline = cors healthcheck versionnegotiation gzip context rootapp
582
+
583
+[composite:rootapp]
584
+paste.composite_factory = glance.api:root_app_factory
585
+/: apiversions
586
+/v1: apiv1app
587
+/v2: apiv2app
588
+
589
+[app:apiversions]
590
+paste.app_factory = glance.api.versions:create_resource
591
+
592
+[app:apiv1app]
593
+paste.app_factory = glance.api.v1.router:API.factory
594
+
595
+[app:apiv2app]
596
+paste.app_factory = glance.api.v2.router:API.factory
597
+
598
+[filter:healthcheck]
599
+paste.filter_factory = oslo_middleware:Healthcheck.factory
600
+backends = disable_by_file
601
+disable_by_file_path = %(disable_path)s
602
+
603
+[filter:versionnegotiation]
604
+paste.filter_factory =
605
+ glance.api.middleware.version_negotiation:VersionNegotiationFilter.factory
606
+
607
+[filter:gzip]
608
+paste.filter_factory = glance.api.middleware.gzip:GzipMiddleware.factory
609
+
610
+[filter:cache]
611
+paste.filter_factory = glance.api.middleware.cache:CacheFilter.factory
612
+
613
+[filter:cache_manage]
614
+paste.filter_factory =
615
+ glance.api.middleware.cache_manage:CacheManageFilter.factory
616
+
617
+[filter:context]
618
+paste.filter_factory = glance.api.middleware.context:ContextMiddleware.factory
619
+
620
+[filter:unauthenticated-context]
621
+paste.filter_factory =
622
+ glance.api.middleware.context:UnauthenticatedContextMiddleware.factory
623
+
624
+[filter:fakeauth]
625
+paste.filter_factory = glance.tests.utils:FakeAuthMiddleware.factory
626
+
627
+[filter:cors]
628
+paste.filter_factory = oslo_middleware.cors:filter_factory
629
+allowed_origin=http://valid.example.com
630
+"""
631
+
632
+
448 633
 class RegistryServer(Server):
449 634
 
450 635
     """
@@ -946,3 +1131,374 @@ class FunctionalTest(test_utils.BaseTestCase):
946 1131
                 self._attached_server_logs.append(s.log_file)
947 1132
             self.addDetail(
948 1133
                 s.server_name, testtools.content.text_content(s.dump_log()))
1134
+
1135
+
1136
+class MultipleBackendFunctionalTest(test_utils.BaseTestCase):
1137
+
1138
+    """
1139
+    Base test class for any test that wants to test the actual
1140
+    servers and clients and not just the stubbed out interfaces
1141
+    """
1142
+
1143
+    inited = False
1144
+    disabled = False
1145
+    launched_servers = []
1146
+
1147
+    def setUp(self):
1148
+        super(MultipleBackendFunctionalTest, self).setUp()
1149
+        self.test_dir = self.useFixture(fixtures.TempDir()).path
1150
+
1151
+        self.api_protocol = 'http'
1152
+        self.api_port, api_sock = test_utils.get_unused_port_and_socket()
1153
+        self.registry_port, reg_sock = test_utils.get_unused_port_and_socket()
1154
+        # NOTE: Scrubber is enabled by default for the functional tests.
1155
+        # Please disbale it by explicitly setting 'self.include_scrubber' to
1156
+        # False in the test SetUps that do not require Scrubber to run.
1157
+        self.include_scrubber = True
1158
+
1159
+        self.tracecmd = tracecmd_osmap.get(platform.system())
1160
+
1161
+        conf_dir = os.path.join(self.test_dir, 'etc')
1162
+        utils.safe_mkdirs(conf_dir)
1163
+        self.copy_data_file('schema-image.json', conf_dir)
1164
+        self.copy_data_file('policy.json', conf_dir)
1165
+        self.copy_data_file('property-protections.conf', conf_dir)
1166
+        self.copy_data_file('property-protections-policies.conf', conf_dir)
1167
+        self.property_file_roles = os.path.join(conf_dir,
1168
+                                                'property-protections.conf')
1169
+        property_policies = 'property-protections-policies.conf'
1170
+        self.property_file_policies = os.path.join(conf_dir,
1171
+                                                   property_policies)
1172
+        self.policy_file = os.path.join(conf_dir, 'policy.json')
1173
+
1174
+        self.api_server_multiple_backend = ApiServerForMultipleBackend(
1175
+            self.test_dir, self.api_port, self.policy_file, sock=api_sock)
1176
+
1177
+        self.registry_server = RegistryServer(self.test_dir,
1178
+                                              self.registry_port,
1179
+                                              self.policy_file,
1180
+                                              sock=reg_sock)
1181
+
1182
+        self.scrubber_daemon = ScrubberDaemon(self.test_dir, self.policy_file)
1183
+
1184
+        self.pid_files = [self.api_server_multiple_backend.pid_file,
1185
+                          self.registry_server.pid_file,
1186
+                          self.scrubber_daemon.pid_file]
1187
+        self.files_to_destroy = []
1188
+        self.launched_servers = []
1189
+        # Keep track of servers we've logged so we don't double-log them.
1190
+        self._attached_server_logs = []
1191
+        self.addOnException(self.add_log_details_on_exception)
1192
+
1193
+        if not self.disabled:
1194
+            # We destroy the test data store between each test case,
1195
+            # and recreate it, which ensures that we have no side-effects
1196
+            # from the tests
1197
+            self.addCleanup(
1198
+                self._reset_database, self.registry_server.sql_connection)
1199
+            self.addCleanup(
1200
+                self._reset_database,
1201
+                self.api_server_multiple_backend.sql_connection)
1202
+            self.addCleanup(self.cleanup)
1203
+            self._reset_database(self.registry_server.sql_connection)
1204
+            self._reset_database(
1205
+                self.api_server_multiple_backend.sql_connection)
1206
+
1207
+    def set_policy_rules(self, rules):
1208
+        fap = open(self.policy_file, 'w')
1209
+        fap.write(jsonutils.dumps(rules))
1210
+        fap.close()
1211
+
1212
+    def _reset_database(self, conn_string):
1213
+        conn_pieces = urlparse.urlparse(conn_string)
1214
+        if conn_string.startswith('sqlite'):
1215
+            # We leave behind the sqlite DB for failing tests to aid
1216
+            # in diagnosis, as the file size is relatively small and
1217
+            # won't interfere with subsequent tests as it's in a per-
1218
+            # test directory (which is blown-away if the test is green)
1219
+            pass
1220
+        elif conn_string.startswith('mysql'):
1221
+            # We can execute the MySQL client to destroy and re-create
1222
+            # the MYSQL database, which is easier and less error-prone
1223
+            # than using SQLAlchemy to do this via MetaData...trust me.
1224
+            database = conn_pieces.path.strip('/')
1225
+            loc_pieces = conn_pieces.netloc.split('@')
1226
+            host = loc_pieces[1]
1227
+            auth_pieces = loc_pieces[0].split(':')
1228
+            user = auth_pieces[0]
1229
+            password = ""
1230
+            if len(auth_pieces) > 1:
1231
+                if auth_pieces[1].strip():
1232
+                    password = "-p%s" % auth_pieces[1]
1233
+            sql = ("drop database if exists %(database)s; "
1234
+                   "create database %(database)s;") % {'database': database}
1235
+            cmd = ("mysql -u%(user)s %(password)s -h%(host)s "
1236
+                   "-e\"%(sql)s\"") % {'user': user, 'password': password,
1237
+                                       'host': host, 'sql': sql}
1238
+            exitcode, out, err = execute(cmd)
1239
+            self.assertEqual(0, exitcode)
1240
+
1241
+    def cleanup(self):
1242
+        """
1243
+        Makes sure anything we created or started up in the
1244
+        tests are destroyed or spun down
1245
+        """
1246
+
1247
+        # NOTE(jbresnah) call stop on each of the servers instead of
1248
+        # checking the pid file.  stop() will wait until the child
1249
+        # server is dead.  This eliminates the possibility of a race
1250
+        # between a child process listening on a port actually dying
1251
+        # and a new process being started
1252
+        servers = [self.api_server_multiple_backend,
1253
+                   self.registry_server,
1254
+                   self.scrubber_daemon]
1255
+        for s in servers:
1256
+            try:
1257
+                s.stop()
1258
+            except Exception:
1259
+                pass
1260
+
1261
+        for f in self.files_to_destroy:
1262
+            if os.path.exists(f):
1263
+                os.unlink(f)
1264
+
1265
+    def start_server(self,
1266
+                     server,
1267
+                     expect_launch,
1268
+                     expect_exit=True,
1269
+                     expected_exitcode=0,
1270
+                     **kwargs):
1271
+        """
1272
+        Starts a server on an unused port.
1273
+
1274
+        Any kwargs passed to this method will override the configuration
1275
+        value in the conf file used in starting the server.
1276
+
1277
+        :param server: the server to launch
1278
+        :param expect_launch: true iff the server is expected to
1279
+                              successfully start
1280
+        :param expect_exit: true iff the launched process is expected
1281
+                            to exit in a timely fashion
1282
+        :param expected_exitcode: expected exitcode from the launcher
1283
+        """
1284
+        self.cleanup()
1285
+
1286
+        # Start up the requested server
1287
+        exitcode, out, err = server.start(expect_exit=expect_exit,
1288
+                                          expected_exitcode=expected_exitcode,
1289
+                                          **kwargs)
1290
+        if expect_exit:
1291
+            self.assertEqual(expected_exitcode, exitcode,
1292
+                             "Failed to spin up the requested server. "
1293
+                             "Got: %s" % err)
1294
+
1295
+        self.launched_servers.append(server)
1296
+
1297
+        launch_msg = self.wait_for_servers([server], expect_launch)
1298
+        self.assertTrue(launch_msg is None, launch_msg)
1299
+
1300
+    def start_with_retry(self, server, port_name, max_retries,
1301
+                         expect_launch=True,
1302
+                         **kwargs):
1303
+        """
1304
+        Starts a server, with retries if the server launches but
1305
+        fails to start listening on the expected port.
1306
+
1307
+        :param server: the server to launch
1308
+        :param port_name: the name of the port attribute
1309
+        :param max_retries: the maximum number of attempts
1310
+        :param expect_launch: true iff the server is expected to
1311
+                              successfully start
1312
+        :param expect_exit: true iff the launched process is expected
1313
+                            to exit in a timely fashion
1314
+        """
1315
+        launch_msg = None
1316
+        for i in range(max_retries):
1317
+            exitcode, out, err = server.start(expect_exit=not expect_launch,
1318
+                                              **kwargs)
1319
+            name = server.server_name
1320
+            self.assertEqual(0, exitcode,
1321
+                             "Failed to spin up the %s server. "
1322
+                             "Got: %s" % (name, err))
1323
+            launch_msg = self.wait_for_servers([server], expect_launch)
1324
+            if launch_msg:
1325
+                server.stop()
1326
+                server.bind_port = get_unused_port()
1327
+                setattr(self, port_name, server.bind_port)
1328
+            else:
1329
+                self.launched_servers.append(server)
1330
+                break
1331
+        self.assertTrue(launch_msg is None, launch_msg)
1332
+
1333
+    def start_servers(self, **kwargs):
1334
+        """
1335
+        Starts the API and Registry servers (glance-control api start
1336
+        & glance-control registry start) on unused ports.  glance-control
1337
+        should be installed into the python path
1338
+
1339
+        Any kwargs passed to this method will override the configuration
1340
+        value in the conf file used in starting the servers.
1341
+        """
1342
+        self.cleanup()
1343
+
1344
+        # Start up the API and default registry server
1345
+
1346
+        # We start the registry server first, as the API server config
1347
+        # depends on the registry port - this ordering allows for
1348
+        # retrying the launch on a port clash
1349
+        self.start_with_retry(self.registry_server, 'registry_port', 3,
1350
+                              **kwargs)
1351
+        kwargs['registry_port'] = self.registry_server.bind_port
1352
+
1353
+        self.start_with_retry(self.api_server_multiple_backend,
1354
+                              'api_port', 3, **kwargs)
1355
+
1356
+        if self.include_scrubber:
1357
+            exitcode, out, err = self.scrubber_daemon.start(**kwargs)
1358
+            self.assertEqual(0, exitcode,
1359
+                             "Failed to spin up the Scrubber daemon. "
1360
+                             "Got: %s" % err)
1361
+
1362
+    def ping_server(self, port):
1363
+        """
1364
+        Simple ping on the port. If responsive, return True, else
1365
+        return False.
1366
+
1367
+        :note We use raw sockets, not ping here, since ping uses ICMP and
1368
+        has no concept of ports...
1369
+        """
1370
+        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1371
+        try:
1372
+            s.connect(("127.0.0.1", port))
1373
+            return True
1374
+        except socket.error:
1375
+            return False
1376
+        finally:
1377
+            s.close()
1378
+
1379
+    def ping_server_ipv6(self, port):
1380
+        """
1381
+        Simple ping on the port. If responsive, return True, else
1382
+        return False.
1383
+
1384
+        :note We use raw sockets, not ping here, since ping uses ICMP and
1385
+        has no concept of ports...
1386
+
1387
+        The function uses IPv6 (therefore AF_INET6 and ::1).
1388
+        """
1389
+        s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
1390
+        try:
1391
+            s.connect(("::1", port))
1392
+            return True
1393
+        except socket.error:
1394
+            return False
1395
+        finally:
1396
+            s.close()
1397
+
1398
+    def wait_for_servers(self, servers, expect_launch=True, timeout=30):
1399
+        """
1400
+        Tight loop, waiting for the given server port(s) to be available.
1401
+        Returns when all are pingable. There is a timeout on waiting
1402
+        for the servers to come up.
1403
+
1404
+        :param servers: Glance server ports to ping
1405
+        :param expect_launch: Optional, true iff the server(s) are
1406
+                              expected to successfully start
1407
+        :param timeout: Optional, defaults to 30 seconds
1408
+        :returns: None if launch expectation is met, otherwise an
1409
+                 assertion message
1410
+        """
1411
+        now = datetime.datetime.now()
1412
+        timeout_time = now + datetime.timedelta(seconds=timeout)
1413
+        replied = []
1414
+        while (timeout_time > now):
1415
+            pinged = 0
1416
+            for server in servers:
1417
+                if self.ping_server(server.bind_port):
1418
+                    pinged += 1
1419
+                    if server not in replied:
1420
+                        replied.append(server)
1421
+            if pinged == len(servers):
1422
+                msg = 'Unexpected server launch status'
1423
+                return None if expect_launch else msg
1424
+            now = datetime.datetime.now()
1425
+            time.sleep(0.05)
1426
+
1427
+        failed = list(set(servers) - set(replied))
1428
+        msg = 'Unexpected server launch status for: '
1429
+        for f in failed:
1430
+            msg += ('%s, ' % f.server_name)
1431
+            if os.path.exists(f.pid_file):
1432
+                pid = f.process_pid
1433
+                trace = f.pid_file.replace('.pid', '.trace')
1434
+                if self.tracecmd:
1435
+                    cmd = '%s -p %d -o %s' % (self.tracecmd, pid, trace)
1436
+                    try:
1437
+                        execute(cmd, raise_error=False, expect_exit=False)
1438
+                    except OSError as e:
1439
+                        if e.errno == errno.ENOENT:
1440
+                            raise RuntimeError('No executable found for "%s" '
1441
+                                               'command.' % self.tracecmd)
1442
+                        else:
1443
+                            raise
1444
+                    time.sleep(0.5)
1445
+                    if os.path.exists(trace):
1446
+                        msg += ('\n%s:\n%s\n' % (self.tracecmd,
1447
+                                                 open(trace).read()))
1448
+
1449
+        self.add_log_details(failed)
1450
+
1451
+        return msg if expect_launch else None
1452
+
1453
+    def stop_server(self, server):
1454
+        """
1455
+        Called to stop a single server in a normal fashion using the
1456
+        glance-control stop method to gracefully shut the server down.
1457
+
1458
+        :param server: the server to stop
1459
+        """
1460
+        # Spin down the requested server
1461
+        server.stop()
1462
+
1463
+    def stop_servers(self):
1464
+        """
1465
+        Called to stop the started servers in a normal fashion. Note
1466
+        that cleanup() will stop the servers using a fairly draconian
1467
+        method of sending a SIGTERM signal to the servers. Here, we use
1468
+        the glance-control stop method to gracefully shut the server down.
1469
+        This method also asserts that the shutdown was clean, and so it
1470
+        is meant to be called during a normal test case sequence.
1471
+        """
1472
+
1473
+        # Spin down the API and default registry server
1474
+        self.stop_server(self.api_server_multiple_backend)
1475
+        self.stop_server(self.registry_server)
1476
+        if self.include_scrubber:
1477
+            self.stop_server(self.scrubber_daemon)
1478
+
1479
+        self._reset_database(self.registry_server.sql_connection)
1480
+
1481
+    def run_sql_cmd(self, sql):
1482
+        """
1483
+        Provides a crude mechanism to run manual SQL commands for backend
1484
+        DB verification within the functional tests.
1485
+        The raw result set is returned.
1486
+        """
1487
+        engine = db_api.get_engine()
1488
+        return engine.execute(sql)
1489
+
1490
+    def copy_data_file(self, file_name, dst_dir):
1491
+        src_file_name = os.path.join('glance/tests/etc', file_name)
1492
+        shutil.copy(src_file_name, dst_dir)
1493
+        dst_file_name = os.path.join(dst_dir, file_name)
1494
+        return dst_file_name
1495
+
1496
+    def add_log_details_on_exception(self, *args, **kwargs):
1497
+        self.add_log_details()
1498
+
1499
+    def add_log_details(self, servers=None):
1500
+        for s in servers or self.launched_servers:
1501
+            if s.log_file not in self._attached_server_logs:
1502
+                self._attached_server_logs.append(s.log_file)
1503
+            self.addDetail(
1504
+                s.server_name, testtools.content.text_content(s.dump_log()))

+ 1020
- 0
glance/tests/functional/v2/test_images.py
File diff suppressed because it is too large
View File


+ 52
- 0
glance/tests/unit/base.py View File

@@ -54,6 +54,34 @@ class StoreClearingUnitTest(test_utils.BaseTestCase):
54 54
         store.create_stores(CONF)
55 55
 
56 56
 
57
+class MultiStoreClearingUnitTest(test_utils.BaseTestCase):
58
+
59
+    def setUp(self):
60
+        super(MultiStoreClearingUnitTest, self).setUp()
61
+        # Ensure stores + locations cleared
62
+        location.SCHEME_TO_CLS_BACKEND_MAP = {}
63
+
64
+        self._create_multi_stores()
65
+        self.addCleanup(setattr, location, 'SCHEME_TO_CLS_MAP', dict())
66
+
67
+    def _create_multi_stores(self, passing_config=True):
68
+        """Create known stores. Mock out sheepdog's subprocess dependency
69
+        on collie.
70
+
71
+        :param passing_config: making store driver passes basic configurations.
72
+        :returns: the number of how many store drivers been loaded.
73
+        """
74
+        self.config(enabled_backends={'file1': 'file', 'ceph1': 'rbd'})
75
+        store.register_store_opts(CONF)
76
+
77
+        self.config(default_backend='file1',
78
+                    group='glance_store')
79
+
80
+        self.config(filesystem_store_datadir=self.test_dir,
81
+                    group='file1')
82
+        store.create_multi_stores(CONF)
83
+
84
+
57 85
 class IsolatedUnitTest(StoreClearingUnitTest):
58 86
 
59 87
     """
@@ -82,3 +110,27 @@ class IsolatedUnitTest(StoreClearingUnitTest):
82 110
         fap = open(CONF.oslo_policy.policy_file, 'w')
83 111
         fap.write(jsonutils.dumps(rules))
84 112
         fap.close()
113
+
114
+
115
+class MultiIsolatedUnitTest(MultiStoreClearingUnitTest):
116
+
117
+    """
118
+    Unit test case that establishes a mock environment within
119
+    a testing directory (in isolation)
120
+    """
121
+    registry = None
122
+
123
+    def setUp(self):
124
+        super(MultiIsolatedUnitTest, self).setUp()
125
+        options.set_defaults(CONF, connection='sqlite://')
126
+        lockutils.set_defaults(os.path.join(self.test_dir))
127
+
128
+        self.config(debug=False)
129
+        stubs.stub_out_registry_and_store_server(self.stubs,
130
+                                                 self.test_dir,
131
+                                                 registry=self.registry)
132
+
133
+    def set_policy_rules(self, rules):
134
+        fap = open(CONF.oslo_policy.policy_file, 'w')
135
+        fap.write(jsonutils.dumps(rules))
136
+        fap.close()

+ 48
- 0
glance/tests/unit/v2/test_discovery_stores.py View File

@@ -0,0 +1,48 @@
1
+# Copyright (c) 2018-2019 RedHat, Inc.
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#    http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12
+# implied.
13
+# See the License for the specific language governing permissions and
14
+# limitations under the License.
15
+from oslo_config import cfg
16
+import webob.exc
17
+
18
+import glance.api.v2.discovery
19
+from glance.tests.unit import base
20
+import glance.tests.unit.utils as unit_test_utils
21
+
22
+
23
+CONF = cfg.CONF
24
+
25
+
26
+class TestInfoControllers(base.MultiStoreClearingUnitTest):
27
+    def setUp(self):
28
+        super(TestInfoControllers, self).setUp()
29
+        self.controller = glance.api.v2.discovery.InfoController()
30
+
31
+    def tearDown(self):
32
+        super(TestInfoControllers, self).tearDown()
33
+
34
+    def test_get_stores_with_enabled_backends_empty(self):
35
+        self.config(enabled_backends={})
36
+        req = unit_test_utils.get_fake_request()
37
+        self.assertRaises(webob.exc.HTTPNotFound,
38
+                          self.controller.get_stores,
39
+                          req)
40
+
41
+    def test_get_stores(self):
42
+        available_stores = ['ceph1', 'file1']
43
+        req = unit_test_utils.get_fake_request()
44
+        output = self.controller.get_stores(req)
45
+        self.assertIn('stores', output)
46
+        for stores in output['stores']:
47
+            self.assertIn('id', stores)
48
+            self.assertIn(stores['id'], available_stores)

+ 32
- 0
glance/tests/unit/v2/test_image_data_resource.py View File

@@ -993,3 +993,35 @@ class TestImageDataSerializer(test_utils.BaseTestCase):
993 993
         self.serializer.stage(response, {})
994 994
         self.assertEqual(http.NO_CONTENT, response.status_int)
995 995
         self.assertEqual('0', response.headers['Content-Length'])
996
+
997
+
998
+class TestMultiBackendImagesController(base.MultiStoreClearingUnitTest):
999
+
1000
+    def setUp(self):
1001
+        super(TestMultiBackendImagesController, self).setUp()
1002
+
1003
+        self.config(debug=True)
1004
+        self.image_repo = FakeImageRepo()
1005
+        db = unit_test_utils.FakeDB()
1006
+        policy = unit_test_utils.FakePolicyEnforcer()
1007
+        notifier = unit_test_utils.FakeNotifier()
1008
+        store = unit_test_utils.FakeStoreAPI()
1009
+        self.controller = glance.api.v2.image_data.ImageDataController()
1010
+        self.controller.gateway = FakeGateway(db, store, notifier, policy,
1011
+                                              self.image_repo)
1012
+
1013
+    def test_upload(self):
1014
+        request = unit_test_utils.get_fake_request()
1015
+        image = FakeImage('abcd')
1016
+        self.image_repo.result = image
1017
+        self.controller.upload(request, unit_test_utils.UUID2, 'YYYY', 4)
1018
+        self.assertEqual('YYYY', image.data)
1019
+        self.assertEqual(4, image.size)
1020
+
1021
+    def test_upload_invalid_backend_in_request_header(self):
1022
+        request = unit_test_utils.get_fake_request()
1023
+        request.headers['x-image-meta-store'] = 'dummy'
1024
+        image = FakeImage('abcd')
1025
+        self.image_repo.result = image
1026
+        self.assertRaises(webob.exc.HTTPBadRequest, self.controller.upload,
1027
+                          request, unit_test_utils.UUID2, 'YYYY', 4)

+ 109
- 0
glance/tests/unit/v2/test_images_resource.py View File

@@ -4267,3 +4267,112 @@ class TestImageSchemaDeterminePropertyBasis(test_utils.BaseTestCase):
4267 4267
     def test_base_property_marked_as_base(self):
4268 4268
         schema = glance.api.v2.images.get_schema()
4269 4269
         self.assertTrue(schema.properties['disk_format'].get('is_base', True))
4270
+
4271
+
4272
+class TestMultiImagesController(base.MultiIsolatedUnitTest):
4273
+
4274
+    def setUp(self):
4275
+        super(TestMultiImagesController, self).setUp()
4276
+        self.db = unit_test_utils.FakeDB(initialize=False)
4277
+        self.policy = unit_test_utils.FakePolicyEnforcer()
4278
+        self.notifier = unit_test_utils.FakeNotifier()
4279
+        self.store = store
4280
+        self._create_images()
4281
+        self._create_image_members()
4282
+        self.controller = glance.api.v2.images.ImagesController(self.db,
4283
+                                                                self.policy,
4284
+                                                                self.notifier,
4285
+                                                                self.store)
4286
+
4287
+    def _create_images(self):
4288
+        self.images = [
4289
+            _db_fixture(UUID1, owner=TENANT1, checksum=CHKSUM,
4290
+                        name='1', size=256, virtual_size=1024,
4291
+                        visibility='public',
4292
+                        locations=[{'url': '%s/%s' % (BASE_URI, UUID1),
4293
+                                    'metadata': {}, 'status': 'active'}],
4294
+                        disk_format='raw',
4295
+                        container_format='bare',
4296
+                        status='active'),
4297
+            _db_fixture(UUID2, owner=TENANT1, checksum=CHKSUM1,
4298
+                        name='2', size=512, virtual_size=2048,
4299
+                        visibility='public',
4300
+                        disk_format='raw',
4301
+                        container_format='bare',
4302
+                        status='active',
4303
+                        tags=['redhat', '64bit', 'power'],
4304
+                        properties={'hypervisor_type': 'kvm', 'foo': 'bar',
4305
+                                    'bar': 'foo'}),
4306
+            _db_fixture(UUID3, owner=TENANT3, checksum=CHKSUM1,
4307
+                        name='3', size=512, virtual_size=2048,
4308
+                        visibility='public', tags=['windows', '64bit', 'x86']),
4309
+            _db_fixture(UUID4, owner=TENANT4, name='4',
4310
+                        size=1024, virtual_size=3072),
4311
+        ]
4312
+        [self.db.image_create(None, image) for image in self.images]
4313
+
4314
+        self.db.image_tag_set_all(None, UUID1, ['ping', 'pong'])
4315
+
4316
+    def _create_image_members(self):
4317
+        self.image_members = [
4318
+            _db_image_member_fixture(UUID4, TENANT2),
4319
+            _db_image_member_fixture(UUID4, TENANT3,
4320
+                                     status='accepted'),
4321
+        ]
4322
+        [self.db.image_member_create(None, image_member)
4323
+            for image_member in self.image_members]
4324
+
4325
+    def test_image_import_image_not_exist(self):
4326
+        request = unit_test_utils.get_fake_request()
4327
+        self.assertRaises(webob.exc.HTTPNotFound,
4328
+                          self.controller.import_image,
4329
+                          request, 'invalid_image',
4330
+                          {'method': {'name': 'glance-direct'}})
4331
+
4332
+    def test_image_import_with_active_image(self):
4333
+        request = unit_test_utils.get_fake_request()
4334
+        self.assertRaises(webob.exc.HTTPConflict,
4335
+                          self.controller.import_image,
4336
+                          request, UUID1,
4337
+                          {'method': {'name': 'glance-direct'}})
4338
+
4339
+    def test_image_import_invalid_backend_in_request_header(self):
4340
+        request = unit_test_utils.get_fake_request()
4341
+        request.headers['x-image-meta-store'] = 'dummy'
4342
+        with mock.patch.object(
4343
+                glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
4344
+            mock_get.return_value = FakeImage(status='uploading')
4345
+            self.assertRaises(webob.exc.HTTPConflict,
4346
+                              self.controller.import_image,
4347
+                              request, UUID4,
4348
+                              {'method': {'name': 'glance-direct'}})
4349
+
4350
+    def test_image_import_raises_conflict_if_disk_format_is_none(self):
4351
+        request = unit_test_utils.get_fake_request()
4352
+
4353
+        with mock.patch.object(
4354
+                glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
4355
+            mock_get.return_value = FakeImage(disk_format=None)
4356
+            self.assertRaises(webob.exc.HTTPConflict,
4357
+                              self.controller.import_image, request, UUID4,
4358
+                              {'method': {'name': 'glance-direct'}})
4359
+
4360
+    def test_image_import_raises_conflict(self):
4361
+        request = unit_test_utils.get_fake_request()
4362
+
4363
+        with mock.patch.object(
4364
+                glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
4365
+            mock_get.return_value = FakeImage(status='queued')
4366
+            self.assertRaises(webob.exc.HTTPConflict,
4367
+                              self.controller.import_image, request, UUID4,
4368
+                              {'method': {'name': 'glance-direct'}})
4369
+
4370
+    def test_image_import_raises_conflict_for_web_download(self):
4371
+        request = unit_test_utils.get_fake_request()
4372
+
4373
+        with mock.patch.object(
4374
+                glance.api.authorization.ImageRepoProxy, 'get') as mock_get:
4375
+            mock_get.return_value = FakeImage()
4376
+            self.assertRaises(webob.exc.HTTPConflict,
4377
+                              self.controller.import_image, request, UUID4,
4378
+                              {'method': {'name': 'web-download'}})

Loading…
Cancel
Save