Library Service¤
The ASAB Library is the concept of shared data content across microservices in the cluster. In the cluster/cloud microservice architectures, all microservices must have access to unified resources. The Library provides a read-only interface for listing and reading this content.
The Library is designed to be read-only. It also allows to "stack" various libraries into one view (overlayed), merging the content of each library into one united space.
The library can also notify the ASAB microservice about changes, e.g. for automated update/reload.
Library structure¤
The library content is organized in a simplified file system manner, with directories and files.
Example of the library structure
Library path rules
- Any path must start with "/", including the root path.
- The directory path must end with "/".
- The directory name cannot contain ".".
- The item path must end with a file extension (e.g. ".txt", ".json", ...).
Layers¤
The library content can be organized into an unlimited number of layers. Each layer is represented by a provider (e.g. filesystem, zookeeper, git, ...) with a specific configuration. Two layers can have the same provider but different base paths.
The layers of the library are like slices of Swiss cheese layered on top of each other. Only if there is a hole in the top layer can you see the layer that shows through underneath. It means that files of the upper layer overwrite files with the same path in the lower layers.
Tip
In the most applications, it is common to create the first layer with Zookeeper provider and the layers beneath with git or libsreg provider. This allows you to quickly modify items of the Library on the first layer.
Disabling files¤
The library concept supports multi-tenancy. By default, all items of the library are visible for everyone, but you can disable some of them for specific tenants.
In order to disable some items of the library, create a file /.disabled.yaml
. This file must be created on the first layer.
In the following example, file1.txt
is disabled for tenant-1 and tenant-2, file2.txt
for tenant-1 and file3.txt
for every tenant.
Warning
When disabling a file for all tenants with a star, don't forget to close it in quotation marks. Otherwise, YAML would interpret star as an alias. Read more about anchors and aliases.
Library service¤
The library service may exist in multiple instances, with different paths
setups.
For that reason, you have to provide a unique service_name
and there is no default value for that.
Each Library item is represented by LibraryItem
dataclass. Read more in the reference section.
Example of the use:
import asab
import asab.library
class MyApplication(asab.Application):
async def initialize(self):
self.LibraryService = asab.library.LibraryService(self, "LibraryService") #(1)!
self.PubSub.subscribe("Library.ready!", self.on_library_ready) #(2)!
async def on_library_ready(self, event_name, library): #(3)!
for item in await self.LibraryService.list("/", recursive=True): #(4)!
print("*", item)
if item.type == 'item': #(5)!
itemio = await self.LibraryService.read(item.name) #(6)!
if itemio is not None:
with itemio: #(7)!
content = itemio.read()
print("- content: {} bytes".format(len(content)))
else:
print(" - (DISABLED)")
if __name__ == '__main__':
app = MyApplication()
app.run()
- Initializes the Library Service. Remember to specify a unique
service_name
. - When the Library is initialized,
Library.ready!
PubSub message is emitted. - The callback has to possess two arguments.
event_name
is the message "Library.ready!",library
is the specific provider with which is the Library initialized. list()
method returns list ofLibraryItem
s. For more information, see the reference section.item.type
can be either 'item' or 'dir'.read()
coroutine returns item IO object or None if the file is disabled.- Item IO object is used as a context manager.
Example of the library configuration:
PubSub messages¤
The Library is created in not-ready
state. After the connection with the technologies behind is established, every library provider changes its state to ready
.
The Library switches to ready
state after all its providers are ready.
If some of the providers is disconnected, the Library switches to not-ready
state again till the connection is reestablished.
Every time the Library changes its state, PubSub
message is published, with the arguments provider
and path
.
Message | Published when... |
---|---|
Library.not_ready! |
at least one provider is not ready. |
Library.ready! |
all of the providers are ready. |
Library.change! |
the content of the Library has changed. |
Notification on changes¤
Some providers are able to detect changes of the library items.
Example
class MyApplication(asab.Application):
async def initialize(self):
self.PubSub.subscribe("Library.ready!", self.on_library_ready
self.PubSub.subscribe("Library.change!", self.on_library_change)
async def on_library_ready(self, event_name, library=None):
await self.LibraryService.subscribe(["/asab"]) #(1)!
def on_library_change(self, message, provider, path): #(2)!
print("New changes in the library found by provider: '{}'".format(provider))
self.LibraryService.subscribe()
method takes either a single path as a string or multiple paths in list and watches for changes in them.- This coroutine takes three arguments:
message
(Library.change!
in this case),provider
(name of the provider that has detected changes) andpath
(the path where changes were made).
Info
Note that the some of the providers detect changes immediately while others detect them periodically. For example, git provider pulls the repository every minute, only after that the changes can be detected.
Providers¤
The list of available providers:
Provider | Read the content | Notify on changes |
---|---|---|
Filesystem | ||
Apache Zookeeper | ||
Microsoft Azure Storage | ||
Git | ||
Libraries repository |
Filesystem¤
The most basic provider that reads data from the local filesystem. The notification on changes functionality is available only for Linux systems, as it uses inotify.
Configuration examples:
Apache Zookeeper¤
ZooKeeper as a consensus technology is vital for microservices in the cluster.
There are several configuration strategies:
1) Configuration from [zookeeper]
section.
[zookeeper]
servers=zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181
path=/library
[library]
providers:
zk://
2) Specify a path of a ZooKeeper node where only library lives.
[zookeeper]
servers=zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181
path=/else
[library]
providers:
zk:///library
[zookeeper]
servers=zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181
path=/else
[library]
providers:
zk:///
3) Configuration from the URL in the [library]
section.
4) Configuration from [zookeeper]
section and joined
[path]{.title-ref} from [zookeeper]
and [library]
sections.
> The resulting path will be [/else/library]{.title-ref}.
[zookeeper]
servers=zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181
path=/else
[library]
providers:
zk://./library
If a path
from the [zookeeper]
section is missing, an application class name will be used, e.g.
/BSQueryApp/library
.
Working with target=tenant Layers¤
The ASAB Library supports multi-tenancy, allowing you to manage and separate content specific to different tenants within layers in library. To handle tenant-specific contexts, use Tenant.set()
to set the context and Tenant.reset()
to reset it after processing.
Implementing Tenant-Specific Logic in Your Application¤
To handle tenant-specific logic, make sure the AuthService
is present in app.py
:
# Install the AuthService for tenant-based authentication
self.AuthService = asab.web.auth.AuthService(self)
self.AuthService.install(self.WebContainer)
Example: Processing Multiple Tenants¤
The following example demonstrates how to iterate through multiple tenants, setting and resetting the tenant context for each one:
import asab.contextvars
async def process_tenants(self):
for tenant in self.Tenants:
# Set the tenant context
tenant_context = asab.contextvars.Tenant.set(tenant)
try:
# Process tenant-specific logic here
print(f"Processing workflows for tenant: {tenant}")
finally:
# Reset the tenant context
asab.contextvars.Tenant.reset(tenant_context)
In this method:
- Tenant.set(tenant)
establishes the context for the current tenant.
- Tenant.reset(tenant_context)
ensures the context is cleared after processing, preventing any unintended carryover.
Example: Handling a Single Tenant¤
If you need to handle only one tenant context, the process is straightforward:
import asab.contextvars
async def process_single_tenant(self, tenant):
tenant_context = asab.contextvars.Tenant.set(tenant)
try:
# Perform operations for the tenant
print(f"Processing data for tenant: {tenant}")
finally:
asab.contextvars.Tenant.reset(tenant_context)
Microsoft Azure Storage¤
You can configure the microservice to read from the Microsoft Azure Storage container.
Configuration:
If Container Public Access Level is not set to "Public access", then "Access Policy" must be created with "Read" and "List" permissions and "Shared Access Signature" (SAS) query string must be added to a URL in a configuration:
[library]
providers: azure+https://ACCOUNT-NAME.blob.core.windows.net/BLOB-CONTAINER?sv=2020-10-02&si=XXXX&sr=c&sig=XXXXXXXXXXXXXX
Git repository¤
Please follow this format in the configuration:
Cloning from GitHub repository:
Using a public repository from GitHub, the configuration may look like this:
Using custom branch:
Use hash #<branch-name>
to clone a repository from a
selected branch:
Deploy tokens in GitLab¤
GitLab uses deploy tokens to enable authentication of deployment tasks, independent of a user account. Authentication through deploy tokens is the only supported option for now.
If you want to create a deploy token for your GitLab repository, follow these steps from the manual:
- Go to Settings > Repository > Deploy tokens section in your repository. (Note that you have to possess a "Maintainer" or "Owner" role for the repository.)
- Expand the "Deploy tokens" section. The list of current Active Deploy Tokens will be displayed.
- Complete the fields and scopes. We recommend a custom "username", as you will need it later for the URL in the configuration.
- Record the deploy token's values before leaving or refreshing the page! After that, you cannot access it again.
After the deploy token is created, use the URL for the repository in the following format:
[library]
providers: git+https://<username>:<deploy_token>@gitlab.example.com/john/awesome_project.git
Where does the repository clone?¤
The git provider clones the repository into a temporary directory. The
default path for the cloned repository is
/tmp/asab.library.git/
and it can be changed manually:
Libraries repository¤
The libsreg
provider downloads the content from the distribution URL.
The distribution URL points to HTTP(S) server where content archives are published.
More than one distribution server can be specified:
This variant provides more resiliency against a distribution server unavailability.
A structure of the distribution server filesystem:
+ /my-library/
- my-library-master.tar.xz
- my-library-master.tar.xz.sha256
- my-library-production.tar.xz
- my-library-production.tar.xz.sha256
- my-library-v43.41.tar.xz
- my-library-v43.41.tar.xz.sha256
...
*.tar.xz
: This is the TAR/XZ archive of the actual content*.tar.xz.sha256
: SHA256 checksum of the archive
The structure of the distribution is as follows:
/{archname}/{archname}-{version}.tar.xz
archname
: A name of the distribution archive,my-library
in the example aboveversion
: A version of the distribution archive,master
,production
are typically GIT branches,v43.41
is a GIT tag.
Tip
This provider is designed to use Microsoft Azure Storage as a distribution point. Is is assumed that the content archives are uploaded to the distribution point using CI/CD.
Reference¤
asab.library.LibraryService
¤
Bases: Service
Configuration:
The order of providers is important, the priority (or layering) is top-down.
Each library provider is specified by URL/URI schema:
zk://
orzookeeper://
for ZooKeeper providerfile://
or local path for FileSystem providerazure+https://
for Microsoft Azure Storage provider.git+https://
for Git provider.libsreg+https://
for Libraries provider.
The first provider is responsible for providing /.disabled.yaml
.
A library is created in “not ready” state, each provider then informs the library when it is ready
(eg. Zookeeper provider needs to connect to Zookeeper servers). Only after all providers are ready, the library itself becomes ready.
The library indicates that by the PubSub event Library.ready!
.
Source code in asab/library/service.py
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 |
|
__init__(app, service_name, paths=None)
¤
Initialize the LibraryService.
The library service is designed to "exist" in multiple instances,
with different paths
setups.
For that reason, you have to provide unique service_name
and there is no default value for that.
If paths
are not provided, they are fetched from [library]providers
configuration.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
app
|
Application
|
The ASAB Application. |
required |
service_name
|
str
|
A unique name of the service. |
required |
paths
|
str | list[str] | None
|
Either single path or list of paths with which LibraryService is connected. |
None
|
Source code in asab/library/service.py
check_disabled(path)
¤
Check if the item specified in path is disabled, either globally or for the specified tenant.
WARNING: When checking for items disabled for a tenant, it must be set in context variable before using this function! If it is not set automatically (e.g. from web request), it must be set manually.
Example:
-
Is path disabled for a specific tenant?
```python try: tenant_ctx = asab.contextvars.Tenant.set(tenant) disabled = self.LibraryService.check_disabled(path) ... finally: asab.contextvars.Tenant.reset(tenant_ctx) ```
-
Is path disabled globally?
```python disabled = self.LibraryService.check_disabled(path) ```
Parameters:
Name | Type | Description | Default |
---|---|---|---|
path
|
str
|
Path to the item to be checked. |
required |
Returns:
Type | Description |
---|---|
bool
|
|
Source code in asab/library/service.py
export(path='/', remove_path=False)
async
¤
Return a file-like stream containing a gzipped tar archive of the library contents of the path.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
path
|
str
|
The path to export. |
'/'
|
tenant
|
str | None
|
The tenant to use for the operation. |
required |
remove_path
|
bool
|
If |
False
|
Returns:
Type | Description |
---|---|
IO
|
A file object containing a gzipped tar archive. |
Source code in asab/library/service.py
find(path)
async
¤
Search for files with a specific name within a library, using the provided path.
The method traverses the library directories, looking for files that match the given filename. It returns a list of paths leading to these files, empty if no items are found.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
path
|
str
|
Location of the file in Library. It must start with a forward slash and include the filename. Example: '/Dashboards/Cisco/Overview.json' |
required |
Returns:
Type | Description |
---|---|
List[str]
|
typing.List[str]: A list of paths to the found files. If no files are found, the list will be empty. |
Source code in asab/library/service.py
get_item_metadata(path)
async
¤
Retrieve metadata for a specific file in the library, including its target
.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
path
|
str
|
The absolute path of the file to retrieve metadata for. Must start with '/' and include a filename with an extension. |
required |
Returns:
Name | Type | Description |
---|---|---|
dict |
Optional[dict]
|
Metadata for the specified file, including |
Source code in asab/library/service.py
is_ready()
¤
Check if all the libraries are ready.
Returns:
Type | Description |
---|---|
bool
|
True if all libraries are ready, otherwise False. |
Source code in asab/library/service.py
list(path='/', recursive=False)
async
¤
List the directory of the library specified by the path that are enabled for the specified tenant. This method can be used only after the Library is ready.
WARNING: Tenant must be set in the context variable! If it is not set automatically (e.g. from web request), it must be set manually.
Example:
try:
tenant_ctx = asab.contextvars.Tenant.set(tenant)
items = self.LibraryService.list(path)
...
finally:
asab.contextvars.Tenant.reset(tenant_ctx)
Parameters:
Name | Type | Description | Default |
---|---|---|---|
path
|
str
|
Path to the directory. |
'/'
|
recursive
|
bool
|
If |
False
|
Returns:
Type | Description |
---|---|
List[LibraryItem]
|
List of items that are enabled for the tenant. |
Source code in asab/library/service.py
open(path)
async
¤
Read the content of the library item specified by path
in a SAFE way, protected by a context manager/with statement.
This method can be used only after the Library is ready.
Example:
async with self.LibraryService.open(path) as io:
if io is None:
return None
text = b.read().decode("utf-8")
Source code in asab/library/service.py
read(path)
async
¤
THIS IS OBSOLETED METHOD, USE open(...)
!!!
Read the content of the library item specified by path
. This method can be used only after the Library is ready.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
path
|
str
|
Path to the file, |
required |
tenant
|
str | None
|
The tenant to apply. If not specified, the global access is assumed. |
required |
Returns:
Type | Description |
---|---|
IO | None
|
Readable stream with the content of the library item. |
Example:
itemio = await library.read('/path', 'tenant')
if itemio is not None:
with itemio:
return itemio.read()
Source code in asab/library/service.py
subscribe(paths, target=None)
async
¤
Subscribe to changes for specified paths of the library.
In order to notify on changes in the Library, this method must be used after the Library is ready.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
paths
|
str | list[str]
|
Either single path or list of paths to be subscribed. All the paths must be absolute (start with '/'). |
required |
target
|
Union[str, tuple, None]
|
In which target to watch the changes. Possible values: - "global" to watch global path changes - "tenant" to watch path changes in tenants - ("tenant", TENANT_ID) to watch path changes in one specified tenant TENANT_ID |
None
|
Examples:
class MyApplication(asab.Application):
async def initialize(self):
self.PubSub.subscribe("Library.ready!", self.on_library_ready
self.PubSub.subscribe("Library.change!", self.on_library_change)
async def on_library_ready(self, event_name, library=None):
await self.LibraryService.subscribe(["/path1/","/path2/"])
def on_library_change(self, message, provider, path):
print("New changes in the library found by provider: '{}'".format(provider))
Source code in asab/library/service.py
asab.library.item.LibraryItem
dataclass
¤
The data class that contains the info about a specific item in the library.
Attributes:
Name | Type | Description |
---|---|---|
name |
str
|
The absolute path of the Item. It can be directly fed into |
type |
str
|
Can be either |
layer |
int
|
The number of highest layer in which this Item is found. The higher the number, the lower the layer is. |
providers |
list
|
List of |
disabled |
bool
|
|
override |
int
|
If |
target |
str
|
Specifies the target context, e.g., "tenant" or "global". Defaults to "global". |