20141001 delapsley-oc-openstack-final
TRANSCRIPT
![Page 1: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/1.jpg)
Enhancing OpenStack Horizon with AngularJS
#openstackmeetupoc
David Lapsley@devlaps, [email protected]
October 1, 2014
![Page 2: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/2.jpg)
OpenStack Horizon in Action
![Page 3: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/3.jpg)
Launching an Instance
![Page 4: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/4.jpg)
Admin Overview
![Page 5: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/5.jpg)
Project Overview
![Page 6: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/6.jpg)
Launching an Instance
![Page 7: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/7.jpg)
Launching an Instance
![Page 8: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/8.jpg)
Launching an Instance
![Page 9: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/9.jpg)
Launching an Instance
![Page 10: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/10.jpg)
Launching an Instance
![Page 11: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/11.jpg)
Launching an Instance
![Page 12: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/12.jpg)
OpenStack CloudsArchitecture and Model
![Page 13: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/13.jpg)
OpenStack Model
http://docs.openstack.org/openstack-ops/content/example_architecture.html
http://docs.openstack.org/training-guides/content/module001-ch004-openstack-architecture.html
![Page 14: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/14.jpg)
OpenStack Projects
● Compute (Nova)
● Network (Nova, Neutron)
● VM Registration (Glance)
● Identity (Keystone)
● Object Storage (Swift, …)
● Block Storage (Cinder)
● Dashboard (Horizon)
![Page 15: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/15.jpg)
OpenStack HorizonArchitecture
![Page 16: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/16.jpg)
Horizon Overview
● Django-based application deployed via
Apache and WSGI
● Provides access to OpenStack services
● Leverages existing technologieso Bootstrap, jQuery, Underscore.js,
AngularJS, D3.js, Rickshaw, LESS CSS
● Extends Django to enhance
extensibility
![Page 17: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/17.jpg)
Django Stack
![Page 18: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/18.jpg)
Horizon Stack
![Page 19: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/19.jpg)
Horizon UINav entries
Column sorting
Linking
RPCData retrieval
Row actions
Table actionsFiltering
Multi-select
![Page 20: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/20.jpg)
Customized UI
![Page 21: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/21.jpg)
AngularJS
![Page 22: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/22.jpg)
AngularJS lets you extend HTML
vocabulary for your application. The
resulting environment is extraordinarily
expressive, readable, and quick to
develop.https://angularjs.org
![Page 23: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/23.jpg)
Develop smaller, lighter web apps that
are simple to create and easy to test,
extend and maintain as they grow
Brad Green, Shyam Seshadri, “AngularJS”
![Page 24: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/24.jpg)
Core concepts
● Model View Controller framework
● Client-side templates
● Data binding
● Dependency injection
![Page 25: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/25.jpg)
Hello Worldindex.html
<html ng-app><head> <script src="angular.js"></script> <script src="controllers.js"></script></head><body> <div ng-controller='HelloController'> <p>{{greeting.text}}, World</p> <button ng-click="action()">Alert</button> </div></body></html>
controllers.js
function HelloController($scope) { $scope.greeting = { text: 'Hello' }; $scope.action = function() { alert('Action!'); };}
AngularJSBy: Brad Green; Shyam SeshadriPublisher: O'Reilly Media, Inc.Pub. Date: April 23, 2013
![Page 26: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/26.jpg)
Hello World
![Page 27: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/27.jpg)
Hello World
![Page 28: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/28.jpg)
AngularJS + Horizon
![Page 29: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/29.jpg)
Horizon Stack Extended
![Page 30: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/30.jpg)
Adding a new PanelUsing current Horizon
![Page 31: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/31.jpg)
Dashboards & Panels
● Horizon provides a flexible framework
for creating Dashboards and Panels
● Panels grouped into PanelGroups
● PanelGroups into Dashboards
![Page 32: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/32.jpg)
Dashboard App
● Dashboards created as Django
Applications
● Dashboard modules partitioned into:o statico templateso python modules
![Page 33: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/33.jpg)
Directory Structureopenstackoc/ __init__.py dashboard.py templates/
openstackoc/ static/
openstackoc/ css/ img/ js/
![Page 34: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/34.jpg)
_10_openstackoc.py
DASHBOARD = 'openstackoc'
DISABLED = False
ADD_INSTALLED_APPS = [
'openstack_dashboard.dashboards.openstackoc',
]
![Page 35: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/35.jpg)
dashboard.pyclass BasePanelGroup(horizon.PanelGroup): slug = "overview" name = _("Overview") panels = ("hypervisors",)
class OpenstackOC(horizon.Dashboard): name = _("OpenstackOC") slug = "OpenstackOC" panels = (BasePanelGroup,) default_panel = "hypervisors" roles = ("admin",)
horizon.register(OpenstackOC)
![Page 36: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/36.jpg)
DashboardDashboard
PanelGroup
![Page 37: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/37.jpg)
Panel● Panels are created as Python Modules● Panel modules partitioned into:o static/o templates/o python modules:
urls.py, views.py, panel.pytables.py, forms.py, tabs.py, tests.py
![Page 38: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/38.jpg)
Directory Structureopenstackoc/ hypervisors/
__init__.py panel.py urls.py views.py
tests.py tables.py templates/
openstackoc/ hypervisors/ index.html static/
openstackoc/ hypervisors /
![Page 39: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/39.jpg)
panel.py
from django.utils.translation import ugettext_lazy as _ import horizon from openstack_dashboard.dashboards.openstackoc import dashboard class Hypervisors(horizon.Panel): name = _("Hypervisors") slug = 'hypervisors' dashboard.OpenstackOC.register(Hypervisors)
![Page 40: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/40.jpg)
DashboardDashboard
PanelGroup
Panel
![Page 41: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/41.jpg)
View Module● View module ties together everything:o Tables, Templates, API Calls
● Horizon base views:o APIView, LoginView, MultiTableView,
DataTableView, MixedDataTableView, TabView,
TabbedTableView, WorkflowView
![Page 42: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/42.jpg)
views.py
from horizon import tables
class HypervisorsIndexView(tables.DataTableView): table_class = hv_tables.AdminHypervisorsTable template_name = ’openstackoc/hypervisors/index.html’
def get_data(self): hypervisors = [] states = {} hypervisors = api.nova.hypervisor_list(self.request) … return hypervisors
![Page 43: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/43.jpg)
Table Module● Table classes provide framework for tables: o consistent look and feelo configurable table_actions and
row_actionso select/multi-select columno sortingo pagination
● Functionality is split server- and client-side
![Page 44: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/44.jpg)
tables.pyclass EnableAction(tables.BatchAction): …
class DisableAction(tables.BatchAction): name = 'disable' classes = ('btn-danger',) def allowed(self, request, hv): return hv.service.get('status') == 'enabled' def action(self, request, obj_id): hv = api.nova.hypervisor_get(request, obj_id) host = getattr(hv, hv.NAME_ATTR) return api.nova.service_disable(request, host, 'nova-compute')
def search_link(x): return '/admin/instances?q={0}'.format(x.hypervisor_hostname)
![Page 45: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/45.jpg)
tables.pyclass AdminHypervisorsTable(tables.DataTable):
hypervisor_hostname = tables.Column( 'hypervisor_hostname', verbose_name=_('Hostname'))
state = tables.Column( lambda hyp: hyp.service.get('state', _('UNKNOWN')).title(), verbose_name=_('State'))
running_vms = tables.Column( 'running_vms', link=search_link, verbose_name=_('Instances'))
...
class Meta: name = 'hypervisors' verbose_name = _('Hypervisors') row_actions = (EnableAction, DisableAction)
![Page 46: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/46.jpg)
Template
● Standard Django template format
● Typically leverage base horizon
templates (e.g. base.html)
![Page 47: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/47.jpg)
index.html{% extends 'base.html' %} {% load i18n horizon humanize sizeformat %} {% block title %}{% trans 'Hypervisors' %}{% endblock %} {% block page_header %} {% include 'horizon/common/_page_header.html' with title=_('All Hypervisors') %} {% endblock page_header %} {% block main %}<div class="quota-dynamic"> <h3>{% trans "Hypervisor Summary" %}</h3> <div class="d3_quota_bar"> <div class="d3_pie_chart" …></div> </div> …</div><div class="row-fluid"> <div class="col-sm-12"> {{ tab_group.render }} </div></div>{% endblock %}
![Page 48: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/48.jpg)
URLs Modules● Provides URL to View mappings
![Page 49: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/49.jpg)
urls.py
from django.conf.urls import patterns from django.conf.urls import url
from openstack_dashboard.dashboards.openstackoc.hypervisors import views
urlpatterns = patterns( 'openstack_dashboard.dashboards.openstackoc.hypervisors.views' url(r'^$', views.IndexView.as_view(), name='index'),)
![Page 50: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/50.jpg)
Completed DashboardNav entries
Column sorting
Panel rendering
Linking
RPCData retrieval
![Page 51: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/51.jpg)
Adding a new Panelwith AngularJS
![Page 52: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/52.jpg)
Directory Structureopenstackoc/ hypervisors/
__init__.py panel.py urls.py views.py
tables.py tests.py templates/openstackoc/hypervisors/ index.html static/openstackoc/hypervisors/js/ hypervisors-controller.jsrest/nova/ __init__.py hypervisor.py instance.py
![Page 53: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/53.jpg)
REST Resource● Provides the source of data via RESTful API
● Resource includes/provides:o entity data/stateo configurable table_actions and
row_actionso sortingo paginationo CRUD operations
![Page 54: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/54.jpg)
hypervisors.pyclass HypervisorResource(resource.BaseNovaResource):
pk = fields.CharField(attribute="pk", _("Primary Key"), hidden=True) hypervisor_hostname = fields.CharField(attribute='hypervisor_hostname', sortable=True, searchable=True) … actions = fields.ActionsField(attribute='actions', actions=[HypervisorViewLiveStats, HypervisorEnableAction, HypervisorDisableAction], title=_("Actions"), sortable=True) class Meta: authorization = auth.RestAuthorization() list_allowed_methods = ['get'] resource_name = '^hypervisor' field_order = ['pk', 'hypervisor_hostname', 'hypervisor_type', 'vcpus', 'vcpus_used', 'memory_mb', 'memory_mb_used', 'running_vms', 'state', 'status', 'actions']
![Page 55: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/55.jpg)
Controller
● Controls view logic
![Page 56: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/56.jpg)
hypervisor-controller.jshorizonApp.controller('TableController', function($scope, $http) { $scope.headers = headers; $scope.title = title; $http.get('/rest/api/v1/nova/instance/').success( function(data, status, headers, config) { $scope.instances = data; }); });
horizonApp.controller('ActionDropdownController', function($scope) { $scope.status = { isopen: false }; $scope.toggleDropdown = function($event) { $event.preventDefault(); $event.stopPropagation(); $scope.status.isopen = !$scope.status.isopen; }; $scope.action = function(action, id) { // Perform action. }; … });
![Page 57: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/57.jpg)
View
● In AngularJS, view is defined in the
HTML template
![Page 58: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/58.jpg)
index.html
{% extends 'base.html' %} {% load i18n horizon humanize sizeformat %} {% block title %}{% trans 'Hypervisors' %}{% endblock %} {% block page_header %} {% include 'horizon/common/_page_header.html' with title=_('All Hypervisors') %} {% endblock page_header %} {% block main %}
![Page 59: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/59.jpg)
index.html
<div ng-controller="TableController"> <table class="..."> <thead> <tr class="..."> <th class="..."> <h3 class="...">{$ title $}</h3> </th> </tr> <tr class="..."> <th class="..." ng-repeat='header in headers'> <div class="...">{$ header.name $}</div> </th> </tr> </thead>
![Page 60: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/60.jpg)
index.html <tr ng-repeat="instance in instances"> <td ng-repeat="datum in instance.data">{$ datum $}</td> <td class="..."> <div ng-controller="ActionDropdownController"> <div class="..." dropdown> <button class="..." ng-click="action(instance.actions[0], instance.name)"> {$ instance.actions[0].verbose_name $} </button> ... <div class="..."> <li class="..." ng-repeat="action in instance.actions"> <a href="#" class="..." ng-click="$parent.action(action,parent.instance.name)"> {$ action.verbose_name $} </a> </li> </ul> </div> </td> </tr> </table>
![Page 61: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/61.jpg)
Advantages
● Clean split between server and client
side
● Significantly cleaner, terser, easier to
understand client-side code
● Significant easier to improve UX
● Client- and server-side code can be
developed and tested independently
● Faster feature velocity
![Page 62: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/62.jpg)
AngularJS + Horizon in Production
![Page 63: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/63.jpg)
Client-side Rendering
“Full” dataset search
Cache up to 1K records client-side
“Full” pagination
![Page 64: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/64.jpg)
Real-time Data Updates every 5s
Increased platform visibility
Every node instrumented
![Page 65: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/65.jpg)
Historical MetricsUp to 1 year of
data
Increased platform visibility
Every node instrumented
Convenient access
![Page 66: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/66.jpg)
OpenStack HorizonContributing
![Page 67: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/67.jpg)
Devstack and Contributing● Devstack:o “A documented shell script to build complete
OpenStack development environments.”o http://devstack.org
● Contributing to Horizon:
– http://docs.openstack.org/developer/
horizon/contributing.html
![Page 69: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/69.jpg)
If this sounds interesting…
http://jobs.metacloud.com
We are hiring!
![Page 70: 20141001 delapsley-oc-openstack-final](https://reader035.vdocuments.us/reader035/viewer/2022070320/5585a50bd8b42ae3228b4912/html5/thumbnails/70.jpg)