Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,5 +303,7 @@ The public contract is the `cloudexit` command, `python -m cloudexit` and `cloud

## License

cloudexit is licensed under the **GNU Affero General Public License v3 (AGPL-3.0)**.
© 2024–2026 EscapeCloud. cloudexit is licensed under the **GNU Affero General Public License v3 (AGPL-3.0)**.
See [LICENSE](./LICENSE) for details.

EscapeCloud, cloudexit and the EscapeCloud logo are trademarks. The AGPL-3.0 licence does not grant any right to use them; modified versions must not use them in a way that suggests they are official.
20 changes: 20 additions & 0 deletions cloudexit/assets/template/egress.html
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,26 @@ <h3>Data Landscape</h3>
</div>
</div>

<div class="px-3 px-md-4 pb-4">
<div class="bg-white shadow-s rounded-4 p-4 mt-3">
<h2 class="h4 mb-3">{{ notices.heading }}</h2>
{% for block in notices.blocks %}
<h3 class="h6 mt-3">{{ block.heading }}</h3>
{% for paragraph in block.paragraphs %}
<p>
{% for part in paragraph %}{% if part is string %}{{ part }}{% else %}<a
href="{{ part[1] }}"
target="_blank"
rel="noopener noreferrer"
>{{ part[0] }}</a
>{% endif %}{% endfor %}
</p>
{% endfor %}
{% endfor %}
<p class="text-muted small mt-4 mb-0">{{ notices.copyright }}</p>
</div>
</div>

<script>
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll(".risk-row").forEach((row) => {
Expand Down
17 changes: 17 additions & 0 deletions cloudexit/assets/template/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,23 @@ <h3 class="h6">{{ summary.closing.how_heading }}</h3>
</p>
{% endif %}
</div>
<div class="bg-white shadow-s rounded-4 p-4 mt-3">
<h2 class="h4 mb-3">{{ summary.notices.heading }}</h2>
{% for block in summary.notices.blocks %}
<h3 class="h6 mt-3">{{ block.heading }}</h3>
{% for paragraph in block.paragraphs %}
<p>
{% for part in paragraph %}{% if part is string %}{{ part }}{% else %}<a
href="{{ part[1] }}"
target="_blank"
rel="noopener noreferrer"
>{{ part[0] }}</a
>{% endif %}{% endfor %}
</p>
{% endfor %}
{% endfor %}
<p class="text-muted small mt-4 mb-0">{{ summary.notices.copyright }}</p>
</div>
</div>
{% endif %}
</main>
Expand Down
8 changes: 3 additions & 5 deletions cloudexit/report/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,10 @@ def draw_footer(canvas, running):
canvas.setFont(FONT_MONO, 6.8)
canvas.drawString(MARGIN, FOOTER_BASELINE, running.document_id)

classification = running.b["cover_classification_value"].upper()
classification_width = canvas.stringWidth(classification, FONT, 6.8)
edition = running.b["footer_edition"].upper()
edition_width = canvas.stringWidth(edition, FONT, 6.8)
canvas.setFont(FONT, 6.8)
canvas.drawString(
(width - classification_width) / 2, FOOTER_BASELINE, classification
)
canvas.drawString((width - edition_width) / 2, FOOTER_BASELINE, edition)
canvas.restoreState()


Expand Down
2 changes: 1 addition & 1 deletion cloudexit/report/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def _rows(self, context, b):
)
left = [
CoverCell(b["cover_doc_id"], self.document_id, MONO_VALUE_STYLE),
CoverCell(b["cover_classification"], b["cover_classification_value"]),
CoverCell(b["cover_edition"], b["cover_edition_value"]),
CoverCell(
b["cover_exit_strategy"], strategy_name(context.strategy_code, b)
),
Expand Down
12 changes: 12 additions & 0 deletions cloudexit/report/egress.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from cloudexit.providers.aws.egress import ARCHIVE_TIERS as AWS_ARCHIVE_TIERS
from cloudexit.providers.azure.egress import ARCHIVE_TIERS as AZURE_ARCHIVE_TIERS
from cloudexit.report.context import scope_facts
from cloudexit.report.notices import notices_html
from cloudexit.report.wording import BLOCKS

PDF_HEADER_TITLE = "EscapeCloud Community Edition - Data & Egress"
Expand Down Expand Up @@ -582,6 +583,14 @@ def _load_estimate(
return meta, rows, totals, pricing, total_fee, type_groups


def _generated_at(timestamp):
# The run's own time, as the estimate stored it; now when it cannot be read
try:
return datetime.strptime(str(timestamp), "%Y-%m-%d %H:%M:%S UTC")
except ValueError:
return datetime.now()


def generate_egress_html_report(report_path: str, json_path: str) -> dict[str, Any]:
try:
meta, rows, totals, pricing, total_fee, type_groups = _load_estimate(
Expand Down Expand Up @@ -625,6 +634,9 @@ def generate_egress_html_report(report_path: str, json_path: str) -> dict[str, A
free_tier_limit_display=summary["free_tier_limit"],
known_data_display=format_bytes(totals["known_size_bytes"]),
type_groups=type_groups,
notices=notices_html(
meta["cloud_service_provider"], _generated_at(meta["timestamp"])
),
)

html_path = os.path.join(report_path, "egress.html")
Expand Down
3 changes: 3 additions & 0 deletions cloudexit/report/executive_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .cover import RecordCover
from .evidence import build_document_information_page
from .flow import chapter_break
from .notices import legal_notices
from .sections import (
alternative_technologies,
costs_overview,
Expand Down Expand Up @@ -45,6 +46,8 @@ def build_content(context, styles, b):
content += glossary(context, styles, b)
content.append(chapter_break())
content += full_assessment(context, styles, b)
content.append(chapter_break())
content += legal_notices(context, styles, b)
return content


Expand Down
4 changes: 4 additions & 0 deletions cloudexit/report/html_sections.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from .brief_summary import collect_brief_facts, finding_rows, lead_text
from .helpers import severity_label
from .notices import notices_html
from .palette import SEVERITIES
from .sections import glossary_entries, scope_boundary, scope_strategy
from .wording import BLOCKS
Expand Down Expand Up @@ -118,4 +119,7 @@ def html_sections(context, b=BLOCKS):
"url": provenance.platform_url,
"assessment_id": provenance.assessment_id,
},
"notices": notices_html(
context.cloud_service_provider, provenance.generated_at, b
),
}
120 changes: 120 additions & 0 deletions cloudexit/report/notices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import re
from xml.sax.saxutils import escape

from reportlab.lib.styles import ParagraphStyle
from reportlab.platypus import Paragraph, Spacer

from .evidence import link_markup
from .fonts import FONT_BOLD
from .sections import (
AWS_ICONS_URL,
AWS_TRADEMARK_URL,
AZURE_ICONS_URL,
LICENCE_URL,
REPOSITORY_URL,
)
from .wording import BLOCKS

NOTICE_BODY = "NoticeBody"
NOTICE_LABEL = "NoticeLabel"
FIELD = re.compile(r"({[a-z_]+})")


def _parts(template, links, values=None):
# A template split into plain text and (text, url) links, so the PDF and
# the HTML are built from the same content
parts = []
for piece in FIELD.split(template):
field = piece[1:-1]
if field in links:
parts.append((links[field], links[field]))
elif values and field in values:
parts.append(str(values[field]))
elif piece:
parts.append(piece)
return parts


def _icon_notice(provider, b):
if provider == 2:
return _parts(
b["notices_icons_aws"],
{"icons_url": AWS_ICONS_URL, "guidelines_url": AWS_TRADEMARK_URL},
)
if provider == 1:
return _parts(b["notices_icons_azure"], {"icons_url": AZURE_ICONS_URL})
return None


def notices_html(provider, generated_at, b=BLOCKS):
# The notices as plain data. A paragraph is a list of text and
# (text, url) links. Only the notice for the report's own provider's icons.
trademarks = [[b["notices_trademark_own"]], [b["notices_trademark_others"]]]
icons = _icon_notice(provider, b)
if icons:
trademarks.append(icons)
return {
"heading": b["notices_heading"],
"blocks": [
{
"heading": b["notices_disclaimer_heading"],
"paragraphs": [
[b["notices_disclaimer"]],
[b["notices_generated"]],
[b["notices_purpose"]],
[b["notices_figures"]],
],
},
{"heading": b["notices_trademarks_heading"], "paragraphs": trademarks},
{
"heading": b["notices_software_heading"],
"paragraphs": [
_parts(
b["notices_software"],
{"repository_url": REPOSITORY_URL, "licence_url": LICENCE_URL},
)
],
},
],
"copyright": b["notices_copyright"].format(year=generated_at.year),
}


def _markup(parts, color):
return "".join(
link_markup(part[1], color) if isinstance(part, tuple) else escape(part)
for part in parts
)


def _styles(styles):
body = styles["ContentStyle"]
return (
ParagraphStyle(NOTICE_BODY, parent=body, fontSize=8.5, leading=12.5),
ParagraphStyle(
NOTICE_LABEL,
parent=body,
fontName=FONT_BOLD,
fontSize=9,
leading=13,
spaceBefore=8,
spaceAfter=2,
textColor=styles["Heading2"].textColor,
),
)


def legal_notices(context, styles, b):
notices = notices_html(
context.cloud_service_provider, context.provenance.generated_at, b
)
color = context.appearance.heading2_color
body, label = _styles(styles)
content = [Paragraph(notices["heading"], styles["Heading1"])]
for block in notices["blocks"]:
content.append(Paragraph(escape(block["heading"]), label))
for parts in block["paragraphs"]:
content.append(Paragraph(_markup(parts, color), body))
content.append(Spacer(1, 10))
content.append(Paragraph(escape(notices["copyright"]), body))
return content
5 changes: 5 additions & 0 deletions cloudexit/report/sections.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@
("escapecloud", "https://escapecloud.io"),
("exitcloud", "https://exitcloud.io"),
)
AWS_ICONS_URL = "https://aws.amazon.com/architecture/icons/"
AWS_TRADEMARK_URL = "https://aws.amazon.com/trademark-guidelines/"
AZURE_ICONS_URL = "https://learn.microsoft.com/en-us/azure/architecture/icons/"
REPOSITORY_URL = "https://github.com/escapecloud/cloudexit/"
LICENCE_URL = "https://github.com/escapecloud/cloudexit/blob/main/LICENSE"


def _text(value, style=ALT_BODY):
Expand Down
71 changes: 68 additions & 3 deletions cloudexit/report/wording.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,15 @@
),
"cover_title_executive_summary": "Executive Summary",
"cover_doc_id": "Document ID",
"cover_classification": "Classification",
"cover_classification_value": "Confidential",
"cover_edition": "Edition",
"cover_edition_value": "Community",
"cover_exit_strategy": "Exit Strategy",
"cover_run": "Assessment Run",
"cover_run_value": "{run} · data as of {completed}",
"cover_generated": "Generated",
"cover_collected_with": "Collected by cloudexit {version} on {date}",
"footer_page_of": "Page {page} of {total}",
"footer_edition": "Community Edition",
"strategy_name_repatriation": "Repatriation to On-Premises",
"strategy_name_hybrid_cloud": "Hybrid Cloud Adoption",
"strategy_name_alternate_cloud": "Migration to Alternate Cloud",
Expand Down Expand Up @@ -368,6 +369,64 @@
),
"fa_uploaded_link": "Open the reports: {link}",
"fa_uploaded_id": "Assessment ID: {assessment_id}",
"notices_heading": "Legal Notices",
"notices_disclaimer_heading": "Disclaimer",
"notices_disclaimer": (
"EscapeCloud Community Edition - This report is provided 'As Is,' "
"without any warranty of any kind. EscapeCloud makes no warranty that "
"the information contained in this report is complete or error-free."
),
"notices_generated": (
"This report was generated by cloudexit from data collected with the "
"organisation's own credentials. Its content was not reviewed or "
"verified by EscapeCloud."
),
"notices_purpose": (
"It is provided for information and planning purposes. It is not "
"legal, financial or investment advice, an audit or an exit plan, and "
"it does not replace independent technical validation. The "
"organisation remains responsible for its own exit planning."
),
"notices_figures": (
"Costs are taken from the provider's billing data for the scope. "
"Results reflect the data of the run shown on page 2 and may change as "
"the environment changes. Alternatives are entries from the data "
"catalogue, not recommendations."
),
"notices_trademarks_heading": "Trademarks",
"notices_trademark_own": (
"EscapeCloud, cloudexit and the EscapeCloud logo are trademarks. The "
"AGPL-3.0 licence does not grant any right to use them; modified "
"versions must not use them in a way that suggests they are official."
),
"notices_trademark_others": (
"Other product and company names in this report, including those of "
"alternative technologies, are trademarks of their respective owners. "
"Naming them does not imply endorsement or affiliation."
),
"notices_icons_aws": (
"AWS service icons are taken from the AWS Architecture Icons "
"({icons_url}); their use is subject to the terms published there and "
"in the AWS Trademark Guidelines ({guidelines_url}). Amazon Web "
"Services and AWS are trademarks of Amazon.com, Inc. or its "
"affiliates. This report is not endorsed by or affiliated with Amazon "
"Web Services."
),
"notices_icons_azure": (
"Azure service icons are taken from the Azure Architecture Icons "
"({icons_url}); their use is subject to Microsoft's icon terms "
"published there. Microsoft and Azure are trademarks of the Microsoft "
"group of companies. This report is not endorsed by or affiliated with "
"Microsoft."
),
"notices_software_heading": "Software",
"notices_software": (
"cloudexit is free software, licensed under the GNU Affero General "
"Public License v3.0 (AGPL-3.0). Source code: {repository_url}. "
"Licence: {licence_url}. The provider icons are not covered by this "
"licence."
),
"notices_copyright": "© 2024–{year} EscapeCloud",
}

# Labels and headings that follow English Title Case, for the wording test.
Expand All @@ -386,7 +445,13 @@
"ev_platform_label",
"cover_title_executive_summary",
"cover_doc_id",
"cover_classification",
"cover_edition",
"cover_edition_value",
"footer_edition",
"notices_heading",
"notices_disclaimer_heading",
"notices_trademarks_heading",
"notices_software_heading",
"cover_exit_strategy",
"cover_run",
"es_heading_introduction",
Expand Down
Loading
Loading